DIR-823G 登录逻辑分析

网上看到DIR-823G有一些未授权的接口,正好自己最近也在研究认证绕过的漏洞,就想借此研究一下DIR-823G的认证机制,所以这篇文章重点是从前端到后端彻底分析清楚DIR-823G是如何进行登录认证的

固件我用的是DIR823GA1_FW102B03

前端

采用FirmAE可以顺利的模拟这个固件,访问可以看到登录界面(这里我选择了跳过向导)

我们随便输入密码,抓包查看一下

这里我输入了两次密码,观察可以发现,每次登录,前端都会向后端发送两个包,我们来分析一下,这里我在实际分析的时候就是先观察的第二个包,所以我们在分析的时候也先看一下第二个包

这里我的密码输入的是123456,而LoginPassword字段是一串数,很明显经过了某种加密,所以这个时候我就尝试找到这里到底是如何加密的,所以用到了前端js逆向技术

审计源代码,我们不难定位到logi.js中的doLogin函数

function doLogin(ifLogin_Password,ifLogin_Captcha)
{
  var PrivateKey = null;
  
  var loginObj = $.Deferred();
  
  var soapAction = new SOAPAction();
  var setLogin = new SOAPLogin();
  var getLogin = new SOAPLoginResponse();
  setLogin.Action = "request";
  setLogin.Username = "Admin";
  setLogin.Captcha = ifLogin_Captcha;

  // Login request
  soapAction.sendSOAPAction("Login", setLogin, getLogin).done(function(obj)
  {
    if (obj.Challenge != null || obj.Cookie != null || obj.PublicKey != null)
    {
      PrivateKey = hex_hmac_md5(obj.PublicKey + ifLogin_Password, obj.Challenge);
      PrivateKey = PrivateKey.toUpperCase();
      // Set Cookie
      $.cookie('uid', obj.Cookie, { path: '/' });
      // Storage data in DOM
    /*try {
               localStorage.setItem("PrivateKey", PrivateKey);
           } catch (e) {
              alert("您的浏览器属于无痕浏览模式,无法进行正常配置,请您将您的浏览器切换成非无痕浏览模式再进行登录");
        return ;
           }*/
       $.cookie('PrivateKey', PrivateKey, {path: '/' }); 
      

      var Login_Passwd = hex_hmac_md5(PrivateKey, obj.Challenge);
      Login_Passwd = Login_Passwd.toUpperCase();
      
      //rewrite login request
      setLogin.Action = "login";
      setLogin.LoginPassword = Login_Passwd;//Login_Passwd;
      setLogin.Captcha = ifLogin_Captcha;
      
      // Do Login to DUT
      var soapAction2 = new SOAPAction();
      soapAction2.sendSOAPAction("Login", setLogin, null).done(function(obj2)
      {
        //for compatibility
        if(obj2.LoginResult == "FAILED")
        {
          loginObj.reject();
        }
        else
        {
          loginObj.resolve();
        }
      })
      .fail(function(){
        loginObj.reject();
      });
    }
    else
    {
      loginObj.reject();
    }
  })
  .fail(function(){
    loginObj.reject();
  });
  return loginObj.promise();

审计这个函数,我们可以知道,在点击登录的那一刻,系统会发首先执行soapAction.sendSOAPAction(“Login”, setLogin, getLogin),这是向后端的Login路由发送第一个包,携带了setLogin和getLogin两个参数,然后将返回值——obj作为参数传给了function这个回调函数,在function中,会再次发送第二个包soapAction2.sendSOAPAction(“Login”, setLogin, null),这个包用来获取最终的登录状态

所以这个时候我们在回过头来看一看第一个包

可以看到,第一个包响应了Challenge,Cookie,PublicKey三个值,而再次回到前端js代码的逻辑

我们可以看到,在程序发出第一个包,接到响应之后,会利用响应中的Challenge和(PublicKey+我输入的密码)进行一次哈希运算,再将运算结果与challenge再一次哈希,得到的才是最终的登录密码,这样做也是避免了密码的明文传输,并且从目前来看,挑战相应能够做到一次一密。

后端

前端的逻辑大概明朗,我们到后端看一下处理逻辑

我们看到,两个包都是通过post请求访问HNAP1路径,所以我们不难定位到如下函数sub_423F90

很明显,sub_42383C就是我们要分析的函数

这里的核心就是一个循环注册路由,所以我们比较容易能够找到路由表,就在LOAD段

映入眼帘的Login,所以我们可以直接分析login函数了

在login函数中,首先会查找xml句柄中的soap:Envelope元素,然后在这个元素中继续查找Action元素,这就像多级嵌套的递归查询,并不难理解,然后找到Action元素之后,会调用mxmlGetText进行取值,如果取值为request,则进入98行代码的逻辑,这也印证了我们第一个包的action

所以接下来的逻辑就是处理第一次发包的逻辑(变量名是我重新命名过的)

可以看到,在接到第一次发包之后,系统会生成challenge,cookie和publickey,这三个数是随机生成的

随机种子是time(0)

然后接下来,程序就会创建一个新的xml实体,把这三个数返回给前端

websAuthResponse函数的作用是根据状态码发送web响应

get_str_by_status_code则是可以根据状态码读取描述信息

200对应的是Data follows

继续往下阅读,可以看到另一条if分支,比对Action的值是否是login

那么这里很明显就是对应第二次发包了

其中,apmib_get(183, byte_58E3F4);用于从flash中获取密码,并存放到byte_58E3F4的位置,apmib_get需要传入两个参数,第一个是索引,第二个是写入的位置,如果你要问为什么183是获取密码,这个我还去简单逆了一下

在apmib_get中,有一个mib_table,跟进可以看到,183对应的是USER_PASSWORD,至于具体是怎么读出来的,那我真不知道了

然后,sub_426570会对读取的密码进行一次小解码,并把结果放在decrypted_config_buffer中

由于不是重点,我们就不过多关注了

接下来的关键:sprintf(combined_challenge_cookie_buffer, “%s%s”, publickey, decrypted_config_buffer);

他会把publickey和decrypted_config_buffer拼接到一起,这和前端的PrivateKey = hex_hmac_md5(obj.PublicKey + ifLogin_Password, obj.Challenge);中obj.PublicKey + ifLogin_Password对上了,也就是前后端在执行同样的hmac_md5处理逻辑

紧接着,后端调用两次hex_hmac_md5处理逻辑,都是利用了challenge的值,这也和前端对上了

hex_hmac_md5调用了命令行工具——hmac_md5,这貌似是他们自己实现的一个工具

这个函数会把命令行的结果存到/tmp/tmpkey中,在读取这个文件,获取结果

最后,程序会比较前端的计算结果和后端的计算结果是否相同,来判断密码是否正确

至此,登录的逻辑基本结束,后面还有一段跟验证码有关的代码,但是这里前端并没有发送验证码,所以后续可以忽略

可以看到,后端完成一次登录过程的日志如上,根据“—>“我们可以溯源到这个位置

也就是原始密码和解码后的密码,但是这里都为空,一开始我就很不理解,以为是不是这里没啥用,后来反应过来,由于我没有初始化,所以密码会不会就是空呢?如果我不输密码,是不是就能成功登录了?

最终,我什么也没输,直接点的登录,结果确实正确

可以看到前后端的计算结果一致

再往后,就是设置一个会话(最多支持20个会话),然后把当前的cookie和private_key放到全局保存起来

未授权漏洞验证

接下来测一下未授权改密码的漏洞,这里我先抓了一个改密码为12345678的包,这里的逻辑是这样的,再点击保存按钮之后,前端会发两个包——CheckPasswdSettings和SetPasswdSettings,前者用于检验当前密码是否正确,后者用于设置新密码,问题就是后者的逻辑并没有依赖前者,就导致我单独发SetPasswdSettings的包,就可以越权重置密码

但事实上,我们可以看到NewPassword字段很明显经过了加密,js逆向可以定位到如下代码

这里使用的是AES加密,密钥是PrivateKey,而这个秘钥,是先前随机生成的Publiskey和真实密码拼接后哈希得到的

所以本质上,我还是无法重置任意用户的密码,因为我不知道原先真实的password

不过我又在想,每次重置密码之后,都要重新登录,那就是一个全新的会话,会生成新的private_key,既然如此,后端应该无法正确解密我的重放包啊,因为这个包用的private_key应该已经过期了才对,可为什么我可以反复重放这个包都能起到效果呢?并且这里的越权是确确实实真实存在的,所以后端到底在怎么处理重置密码的逻辑呢?

更让我费解的是,在测试的过程中,我发现,如果把NewPassword删掉一部分,再重放,您猜怎么着?嘿,密码变成空了,并且,cookie可以为空!(或许跟刚刚说的无法重置有冲突,因为这是我真实的思考过程,根据目前的测试,确实可以重置任意用户的密码为空)

问题现在有点多了,我们来逐一捋一下

1.为什么重置密码功能可以越权访问

2.后端到底如何还原密码明文

3.为什么当我删掉cookie,NewPassword,可以将密码重置为空

首先来回答第一个问题,其实不仅重置密码功能,凡是HNAP1路径下的处理函数,貌似都可以越权访问,究其原因在于goahead服务的鉴权逻辑

可以看到,websUrlHandlerDefine函数有多个参数,其中比较重要的是第一个参数——路径,第四个参数——处理函数,第五个参数——优先级(1表示所有数据包都需要首先经过该回调函数进行处理,通常用来做数据包鉴权,路径合法性判断,未授权访问路径定义等,0为默认,通常用来定义认证后可以访问到的接口逻辑实现,2表示没有回调函数匹配的数据包会通过该回调函数处理),只有当优先级为1的handler函数返回值为0(意味着通过),才能根据路径匹配优先级为0的handler函数

所以,优先级为1的websSecurityHandler函数就是用来鉴权的,我们跟进查看

这里几个函数是用于从请求头中获取对应位置的值的,调试过程中发现都是0,所以暂且忽略

随后,程序会检测是否携带cookie,并且当前会话在10分钟内有活动,如果10分钟内都没有活动,那就清除会话

然后,程序会检测是否访问的是特定路径

我们访问的是/HNAP1/,所以不符合上述判断,不会执行上面的代码,随后,会进入如下逻辑

geturl函数,根据调试可以确定这里的url返回值是”/”,传入的参数是”/HNAP1/”

然后,到了最关键的GetUrlAuthMethod函数,这是权限访问的核心,这里我们先跳过分析,只知道目前这个函数的返回值为3

由于v12=3,v17为空,所以会执行到如下位置

而sub_410F10函数返回值恒为0,所以最终sub_4110F4的返回值也一定为0

然后,程序就会进入websHNAPHandler函数,处理HNAP1路径的请求

这里使用了strstr进行查找,查找soapaction中是否出现了handler中的路径,除此之外并没有其他校验了,以至于我构造这样的soapaction,程序都能正确响应,并返回GetDeviceSettings的结果,因为路由表中GetDeviceSettings位置更靠前

登录功能:🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡

那么为什么GetUrlAuthMethod函数会返回3呢?这就需要我们分析一下初始化函数了

首先,程序初始化了一个数据库,并将其绑定到了一个全局变量——data_base_root

然后,AddUserGroup会添加一个组,租的名称是“adm”,权限为7,认证方法为3

AddUser函数会在“adm”组里添加一个用户,用户名为“admin”,密码为“1234”,然后AddAccessLimit函数,会对在用户组“adm”中,访问路径为“/”的添加访问控制,认证方法为3,就是说访问“/”的时候,触发的访问策略属于用户组“adm”,控制方法为3

这个时候我们在跟进到GetUrlAuthMethod函数里

这个函数,首先会根据访问的路径,反查属于的组别,因为我们访问的是“/”,所以属于adm这个用户组

然后,会根据当前组别,反查控制方法,由于我们已经查到了组别是”adm”,所以我们查到了它对应的控制方法是3,所以最终返回的是3

那么为什么输入”/HNAP1/”,会返回“/”呢?让我们跟进到geturl函数

这个函数的作用是,检查当前路径是否在访问控制表中被注册过,如果没被注册过,就递归到父目录

由于我们只注册了“/”,所以最终程序会递归到”/”,对应的访问方法为3,导致了越权访问

接下来回答第二和第三个问题

首先,我们前面的实验已经知道,后端计算密码不依赖cookie的值,那么具体依赖什么呢?这里先给出答案,就是HNAP_AUTH这个字段

js逆向,我们可以得知,HNAP_AUTH共两部分,后面一部分是时间戳,前面一部分是根据时间戳,soapaction和private_key哈希得到的值,这里面保证身份合法的是private这个值

回到后端,我们进入设置密码的函数

跟进到check_and_get_password函数,可以看到,这个函数把原始的(经过加密的)密码作为参数穿了进去

在当前发包携带了cookie的情况下,会执行如下逻辑,首先,会利用strstr函数,查找session_table中的uid是否在cookie中,也就是当前cookie是否是合法会话,如果是,那么就会检验hnap_auth1(HNAP_AUTH前半部分)收否有值,有的话,将hnap_auth2和soapaction拼接到一起,对应了前端的拼接逻辑,然后md5加密一下,并将加密后的值和hnap_auth1比较,也是完全按照前端的逻辑,这样,如果是合法用户,那么它的private_key和后端存储的就是相同的,二者加密后结果相同,就可以利用这个私钥进行AES解密了,解完密之后,在进行一个小解码,就可以返回newpassword的明文了

在AES解密中,如果密文长度错误,就不会执行任何解密逻辑,直接返回-1

而程序依然会执行将解密后的明文v13复制到a4上的逻辑,由于v13一开始就被赋0了,并且AES没有对其进行任何赋值,就导致最终的解密明文就是0,于是解释了为什么删掉一块可以将密码置空的原因

那么,我接下来又做了一次尝试,将路由器重启,但是重放之前修改密码的包,还是成功将密码置空,这是什么原因呢?

这里不得不调试看一下了

可以看到,程序确实接收到了cookie,但问题是,由于路由器刚刚重启,没有任何新的会话,就算有,也不可能有我现在cookie中的uid,所以程序在执行到17行的if判断时,就会不通过(如果有回话,那就是第19行的if不通过),所以程序就会返回空密码

如果v26为空,就会执行到第70行的ambit_set函数,这个函数就是将dword_4AA9F0的值设置成密码,一开始我也不知道这个值是多少,他初始化为0

查看他的交叉引用,可以知道对它的所有引用都是取值操作,没有赋值操作,所以我们推断这里就是0

动态调试到对应位置,发现这里确实是0

由此,也就解释了为什么重放随便一个包,都能重置密码为空

总结下来这个登录认证的逻辑看起来有挑战响应,貌似很安全,但是其后续的身份校验做的属实拉胯

还有一个值得一提的就是,登录过程中,后台会输出明文密码,即使前端输入的密码不正确,后端也能显示正确密码,又由于这个件有一个认证前RCE,所以可以很轻易泄露密码,不过既然有认证前RCE了,这个攻击好像也就比较鸡肋了

评论

  1. Acker
    2 月前
    2026-5-19 18:10:34

    学习大佬思路

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇