作为一个或许已经过气了的漏洞,已经很少在实战场景中再去使用他了。但是其背后的原理,值得我们学习。
php 5.3.4中php修复了0字符
1.php:
xxxxxxxxxx
<?php
include "1.txt\000.jpg";
?>
1.txt
xxxxxxxxxx
<?php
echo 'fireXXX';
?>
存在漏洞的版本会成功解析,即将1.txt\000.jpg变成为1.txt并成功包含
我们使用PHP5.2.17去分析:
php去解析1.php文件,主要的执行流程在Zend/zend.c
的zend_execute_scripts
函数中。该函数首先通过zend_compile_file
获取1.php
文件的内容,然后调用zend_execute
解析读取到的文件内容。
zend_compile_file
函数首先调用open_file_for_scanning
去读取文件,然后通过zendparse
去进行语法和词法解析,而zendparse
是通过lex_scan
去扫描出token
并进行语法分析。可以通过调试器观察到include的文件名参数经过lex_scan
后的数据:
这里就能发现,虽然长度为10,但是文件名只剩下了1.txt
其实看了这么多流程,归根结底,还是因为C语言的"问题"。因为C语言在处理字符串
的时候,是以\0结尾的。
于是我们尝试自己手动给他加上\0
看看C语言最终获取到的是什么
现在,我们就知道究竟是什么造成了PHP出现00截断这个漏洞了。
那PHP官方是怎么修复的呢,我们来看看
修复的代码位于ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER
函数的开始处:
xxxxxxxxxx
if (Z_LVAL(opline->op2.u.constant) != ZEND_EVAL && strlen(Z_STRVAL_P(inc_filename)) != Z_STRLEN_P(inc_filename)) {
if (Z_LVAL(opline->op2.u.constant)==ZEND_INCLUDE_ONCE || Z_LVAL(opline->op2.u.constant)==ZEND_INCLUDE) {
zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, Z_STRVAL_P(inc_filename) TSRMLS_CC);
} else {
zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, Z_STRVAL_P(inc_filename) TSRMLS_CC);
}
}
该代码中:
xxxxxxxxxx
strlen(Z_STRVAL_P(inc_filename)) != Z_STRLEN_P(inc_filename)
Z_STRVAL_P(inc_filename)
即上图中的val
,即"1.txt
",strlen取得长度为5,而Z_STRLEN_P(inc_filename)
即上图中的len即10。
一旦出现%00截断,include的文件名经过url转码由"1.txt%00.jpg"变为"1.txt\000.jpg",进入php语法词法分析器解析后会将这个字符串解析成一个字符串,并使用zend_scan_escape_string
进行字符串转码,如图,进入zend_scan_escape_string
的内容为:
中间的\000还被解析为4个字符,转码中会将他当作八进制数据转成一个字符\0,因此最终1.txt\000.jpg长度是10。
只要比较发现文件名的strlen长度和语法分析出来的长度不一样,就说明内部存在截断的字符,因此输出了打开文件失败的信息。
用小Demo来演示就是:
看完了PHP的00截断,我们再来看看JAVA的00截断漏洞又是因为什么产生的呢?
xxxxxxxxxx
import java.io.*;public class T2 {
public static void main(String[] args) {
String filepath = "c://shell.jsp" + (char)0 + ".txt";
String content = "Test by c0ny1";
System.out.println(filepath); try {
FileOutputStream fos = new FileOutputStream(filepath);
fos.write(content.getBytes());
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
}
}
通过在漏洞版本和非漏洞版本运行以上代码,可知如果00截断成功,则会在系统的c盘根目录新建一个内容为Test by c0ny1
的shell.jsp
,如果没有截断成功,则抛出Invalid file path
异常。
我选择使用JDK1.7.0
(JDK1.7第一个版本),来跟踪漏洞测试代码从运行到触发。
将传进来的name参数作为路径,新建了File对象,再次传入到FileOutputStream
对象新的构造函数。根据传入的两个参数的类型,我们可以确定会进入到以下这个构造函数。
FileOutputStream对象的构造方法又调用了open函数,打开了name参数传进来的文件路径,我们继续跟进open函数。
发现open函数是一个native method。它的实现体是由非java语言(c语言)实现的。只能去OpenJDK官网下载jdk源码来查看它的实现。无奈没有找到jdk7u1的源码,只找到了jdk7u75的源码。其实在小版本上源码应该区别不大。
在\openjdk\jdk\src\windows\native\java\io\FileOutputStream_md.c
中找到了FileOutputStream
类的open
方法的JNI实现。open方法又调用了fileOpen
方法,继续跟进fileOpen方法。
在io_util_md.c
中找到了fileOpen
方法的定义。
xxxxxxxxxx
jlong winFileHandleOpen(JNIEnv *env, jstring path, int flags)
{
......
if (onNT) { //如果在Windows NT/Windows 2000操作系统下
WCHAR *pathbuf = pathToNTPath(env, path, JNI_TRUE);
if (pathbuf == NULL) {
/* Exception already pending */
return -1;
}
h = CreateFileW(
pathbuf, /* Wide char path name */
access, /* Read and/or write permission */
sharing, /* File sharing flags */
NULL, /* Security attributes */
disposition, /* creation disposition */
flagsAndAttributes, /* flags and attributes */
NULL);
free(pathbuf);//创建文件
} else {
WITH_PLATFORM_STRING(env, path, _ps) {
h = CreateFile(_ps, access, sharing, NULL, disposition,flagsAndAttributes, NULL);//创建文件
}
END_PLATFORM_STRING(env, _ps);
}
......
return (jlong)h;
}
通过阅读以上代码,可知如果在Windows NT/Windows 2000平台下会调用pathToNTPath
函数将原始文件路径转化为Windows NT系统合法路径。然而通过阅读该方法源码,发现它并没有对\00字符串进行过滤。如果在其他Window操作系统版本下,则直接使用原始文件路径。
按照winFileHandleOpen
方法的逻辑,无论如何最终都是调用了CreateFileW
这个Windows API函数来创建文件。由于这个过程中均未对\00
字符串进行过滤,如果传入的文件路径带有\00字符,则CreateFileW
函数在创建文件时,路径会被截断。这没什么好说的。
这里我们没法继续跟进CreateFileW函数,毕竟Windows不开源。为了文章的严谨性,这里我用C语言写一个demo,来证明该函数可以截断。
xxxxxxxxxx
#include "windows.h"
int main()
{
HANDLE fileHandle = CreateFileW(L"C:\\shell.jsp\0test.txt", GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
char *data = "Test by c0ny1";
DWORD a = strlen(data);
unsigned long b;
WriteFile(fileHandle, data, a, &b, NULL);
CloseHandle(fileHandle);
return 0;
}
运行会创建一个shell.jsp
这里选择使用jdk1.7.0_80
(JDK1.7最新版本),来观察漏洞如果被修复的。
我们继续按照原来漏洞触发的调用链重新跟踪一遍,跟踪到第二构造函数时,发现多了一个针对文件路径的检查,若检查结果为非法,则抛出异常Invalid file path
.
继续跟进,来到java.io.File
类的isInvalid
方法,发现该检查函数判断了路径中是否包含00字符串。(注意:java默认编码为Unicode,00字符串的Unicode编码为\u0000)。
这里与PHP修复方案还是不太一样的,JAVA对于这种格式是直接报错,而PHP则会贴心的给你转义以后继续让你使用。
我们知道jdk1.7版本是部分版本存在漏洞的。但这里我们需要确定是哪个版本修复了这个漏洞。翻阅了JDK1.7多个版本代码,发现在JDK1.7.0_40(7u40)开始加上了对文件名是否存在\00字符的检查。也就是说 JDK1.7.0_40之前java是存在00截断的,而之后的版本就不存在了!
后面在官网的JDK 7u40的更新日志中也找到了关于00截断问题Bug ID,分别为JDK-8003992
和JDK-8011539
,具体链接放在了文末的参考文章里了。其实这两个是同一个Bug,官网也说明了它们重复了。