安装包用 7-Zip 即可完整解包,内部是一个 Electron 应用(Chromium + Node.js)。没有 asar 打包、没有代码混淆、没有加密,17,030 个文件全部以明文形式躺在磁盘上,其中 9,352 个 JavaScript 源文件可读。
这不是"反汇编级别"的逆向,而是"直接读源码"级别。整个系统的业务逻辑、数据库结构、接口协议、加密密钥,全部一览无余。
| 项目 | 内容 |
|---|---|
| 应用名称 | FO-ST(Front Office – Standard) |
| 版本 | 8.1.202607021300 |
| 开发者 | hi-thinking <developer@hi-thinking.com> |
| 技术形态 | Electron 桌面应用(Chromium + Node.js) |
| 安装包 | Nullsoft NSIS v3.04,需管理员权限(requireAdministrator) |
| 入口 | main.js(主进程,74 KB) |
| 前端页面 | 1,509 个 HTML,jQuery + jqGrid 风格的传统多页应用 |
| 库 | 版本 | 用途 |
|---|---|---|
@grapecity/activereports | ^5.1.3 | 报表引擎(商业授权组件) |
nedb / nedb-promises | ^1.8.0 / ^6.2.3 | 本地嵌入式数据库 |
lokijs | ^1.5.11 | 本地内存数据库 |
crypto-js | ^4.1.1 | 加解密(AES/MD5) |
electron-store | ^6.0.1 | 本地配置持久化 |
electron-updater | 4.3.5 | 自动更新 |
pdfkit / xlsx | ^0.15.1 / ^0.18.5 | PDF / Excel 导出 |
streamsaver | ^2.0.6 | 大文件流式下载 |
后端是 Java Web 应用,部署包名为 huasheng(华胜,厂商内部代号),前端通过 HTTP 与之通信:
服务地址配置:$.HSAPI_SERVER = {客户端设置}[默认模板值 192.168.1.156:8080]
↓ 可在登录页齿轮⚙️中修改
接口基址: $.HSAPI_AJAXURL = HSAPI_SERVER + "/" + HSAPI_PACKAGENAME
例:http://v8.argylehotels.com:8089/huasheng
注:文档中提到雅阁酒店实际使用 v8.argylehotels.com:8089(酒店编号 1001–1054)和 crsv8.argylehotels.com:8089(1055 以上)。
系统几乎所有数据操作都走 同一个"万能接口",这是整个架构最关键的设计:
POST {服务器}/huasheng/qms/mobile/procedure2
Headers: token / hotelId
请求体(AES 加密):
selectname → 存储过程名
sqlwhere → 查询条件(AES-ECB 加密)
... → 分页/排序参数
响应:JSON
四种调用变体:
/qms/mobile/procedure2 —— 主力接口,调用 968 次/qms/mobile/procedure3 —— 36 次/qms/mobile/procedure4 —— 23 次/qms/mobile/procedure2withoutToken —— 免登录接口,10 次(需关注)本质是一个"通用 SQL 执行网关":前端传存储过程名和条件,后端执行后返回结果集。这种设计让后端无需为每个功能写接口,但把所有业务逻辑压在了数据库存储过程里。
共提取 74 个端点,以下按调用频次排序(已过滤静态资源):
| 端点路径 | 调用次数 |
|---|---|
/qms/mobile/procedure2 | 968 |
/qms/mobile/procedure3 | 36 |
/qms/mobile/procedure4 | 23 |
/qms/mobile/procedure2withoutToken | 10 |
/frontoffice/front/imageUtil/saveIdcard | 9 |
/qms/mobile/logout | 7 |
/hobill/bill-pdf | 7 |
/attach/ | 5 |
/frontoffice/sys/sysuser/editPassword | 5 |
/qms/mobile/getToken | 4 |
/frontoffice/sys/sysuser/home#page/reservation_new?&showmenu=yes&returnOrClose=close | 4 |
/qms/mobile/login_check | 4 |
/qms/mobile/download-word-file | 4 |
/file/ | 4 |
/excel/convert | 3 |
/qms/mobile/procedure | 3 |
/weChat/payment_temp | 3 |
/efd/bill | 3 |
/efd/devices | 3 |
/frontoffice/dead-lock | 3 |
/account/export | 2 |
/byyy/export | 2 |
/h2/getH2DataSet/h2_roomsview | 2 |
/qms/mobile/login | 2 |
/qr-code-base64 | 2 |
/login/housekeeping/customLogin.jsp?&hotelId= | 2 |
/al/payment_temp | 2 |
/weChat/payment_refund | 2 |
/al/payment_refund | 2 |
/weChat/payment_checkout | 2 |
/al/payment_checkout | 2 |
/weChat/payment_credit | 2 |
/al/payment_credit | 2 |
/qms/mobile/upload-word-file | 2 |
/static/hotel/report/js/plug-ins/business/ajaxfileupload.js | 2 |
/frontoffice/front/imageUtil/saveMarcust_img | 2 |
/account/export? | 1 |
/excel/export64 | 1 |
/mfa-qrcode | 1 |
/qms/mobile/proceduregrid | 1 |
/qms/mobile/procedure2Post | 1 |
/qms/mobile/procedure2bypass | 1 |
/upload/file | 1 |
/arCustDebitPosting/payment_temp | 1 |
/arCustDebitPosting/payment_checkout | 1 |
/frontoffice/sys/sysuser/home#page/rsv_group?&returnOrClose=close& | 1 |
/frontoffice/front/imageUtil/couponVoucher | 1 |
/frontoffice/sys/sysuser/home#page/reservation_new?&showmenu=yes&returnOrClose=return | 1 |
/frontoffice/sys/sysuser/home#page/reservation_new?&gno= | 1 |
/frontoffice/front/imageUtil/getFacePhotoDb | 1 |
/frontoffice/front/imageUtil/saveProfilePhotoDb | 1 |
/frontoffice/front/imageUtil/getProfilePhotoDb | 1 |
/frontoffice/sys/sysuser/decryptPassword | 1 |
/weChat/payment_encode_wechar | 1 |
/weChat/payment_encode_ali | 1 |
/frontoffice/front/wordModalOper/downloadWord | 1 |
/frontoffice/front/wordModalOper/uploadWordFile | 1 |
/hobill/bill-srd-download | 1 |
/hobill/srd-save | 1 |
/efd/send-data | 1 |
按源文件数量排序,反映各模块的复杂度:
| 模块 | 文件数 |
|---|---|
systemSettings | 476 |
reservation | 384 |
js | 307 |
assets | 162 |
memberManagement | 150 |
feast | 138 |
roomsManagement | 126 |
stStoreIn2 | 122 |
auditQuery | 116 |
ar | 108 |
cashiering | 104 |
housekeeper | 88 |
coupons | 85 |
sales | 81 |
miscellaneous | 76 |
report | 71 |
group | 53 |
quickKeys | 50 |
maintenance | 48 |
components | 42 |
oms | 39 |
busReservation | 36 |
upgradeDeploy | 36 |
pmscs_ota | 34 |
sc | 20 |
frontDesk | 20 |
initdata | 20 |
otaTicket | 16 |
homepage | 14 |
document | 14 |
anewSummarize | 12 |
custom | 12 |
(根) | 10 |
html | 10 |
easyFrontDesk | 8 |
programmer | 8 |
test | 6 |
测试 | 4 |
repastReports | 2 |
salesCatering | 2 |
这是本次逆向最有价值的成果。系统用 1,341 个 .db 文件来描述每个页面组件,每个文件是 JSON 文本,里面直接写着:
{{"cpname":"grid", "tablename":"hotel_cost_set",
"procedurename":"ALINCustomSelect_Plus30",
"fields":"id,rcode,item,sn,code,formula,..."}
等于把整个数据库的表名、字段名、存储过程名、页面映射关系全部交了出来。汇总后得到 364 张表、5,630 个字段、60 个存储过程。
| 表名 | 字段 | 存储过程 | 字段示例 |
|---|---|---|---|
viewJqGrid_rsv_profile_list | 117 | ALINCustomSelect_Plus30 | Bl_mess, Blacklist, Mailing, Potential_night, Potential_revenue, Vip, acc_contact, addr… |
viewJqGrid_ho_guest_listAll | 83 | ALINCustomSelect, ALINCustomSelect_Normal, ALINCustomSelect_Plus30 | acc_name1, acc_name2, acc_name3, acc_name4, adults, agent, agent_id, arr_c… |
View_pms_invoice | 76 | ALINCustomSelect_Plus30 | bill_invoice_amount, bmb_bbh, byzd1, byzd2, byzd3, byzd4, byzd5, bz… |
pms_invoice | 74 | ALINCustomSelect_Plus30 | bmb_bbh, byzd1, byzd2, byzd3, byzd4, byzd5, bz, chbz… |
viewJqGrid_ho_marcust_arcust | 73 | ALINCustomSelect_Plus30 | address, allsearch, amount, ar_aname, ar_balance, ar_gno, ardate, balance_c… |
viewOMS_oms_order_head | 68 | ALINCustomSelect_Plus30 | approved_at, approver_id, approver_name, approver_note, assigned_at, assignee_dept, assignee_id, assignee_name… |
viewGridPMS_Reservation | 68 | ALINCustomSelect_Plus30 | Status, accname, adults, allsearch, arr_d, arr_d_c, arr_t, balance_c… |
viewJqGridtcoupon_cpcode | 65 | ALINCustomSelect_Normal, ALINCustomSelect_Plus30 | act_code, act_expirydays, act_rate, act_rule, act_startdays, amount, apptype, ava_bmh… |
viewGridPMS_ReservationCheck | 63 | ALINCustomSelect_Plus30 | Contact_company, Status, adults, arr_d, arr_d_c, arr_d_end, arr_t, arr_t_end… |
viewGridPMS_ReservationAll | 62 | ALINCustomSelect_Plus30 | Status, accname, adults, arr_d, arr_d_c, arr_t, building, child… |
viewJqGrid_ho_ArApFr_ledger | 62 | ALINCustomSelect_Plus30 | Ftype, adjust, amount, arcrtype, balance, balance1, balancedate, co_text4… |
viewJqGridtCoupon_ledger | 57 | ALINCustomSelect_Normal, ALINCustomSelect_Plus30 | act_code, act_rate, act_rule, actived, actived_by, actived_status, available, cancel… |
view_ct_yygk_fb_ct_lszd | 55 | ALINCustomSelect_Normal, ALINCustomSelect_Plus30 | BMMC, DISPRE, JDH, JZRQ, JZSJ, KTRQ, KTSJ, MC… |
viewGridPMS_WHSK_ServiceCenter | 50 | ALINCustomSelect_Plus30 | beginning_time, crdate, duration, finished, finished_time, finisheddate, floor_c, getinfo… |
viewJqGrid_ct_invoice | 49 | ALINCustomSelect_Plus30 | account, address, ar_coid, ar_no, bank, bmh, bmh_c, checker… |
VIEWALL_ho_ftp_setup | 48 | ALINCustomSelect_Plus30 | block, editdefault, ftptype, ico_arrival, ico_ass, ico_assz, ico_checkedin, ico_com… |
viewJqGrid_rsv_profile_listall | 46 | ALINCustomSelect_Plus30 | Bl_mess, Blacklist, addr, agent, birthday, car_no, city, cname… |
viewGridPMSHo_gledgerFolioHistory | 42 | ALINCustomSelect_Plus30 | ar, arr_d_c, audit_status, ca, cc, closedate, co_name, co_room… |
viewlistpms_reservationhis | 40 | ALINCustomSelect_Plus30 | Status, adults, arr_d, charge, child, co_mess, comment, comment1… |
viewGridPMS_ho_arledger | 37 | ALINCustomSelect_Plus30 | aname, codate_c, coid, credit, crtype, date_c, dcode, dcode_c… |
viewGridPMS_ho_group | 36 | ALINCustomSelect_Plus30 | allsearch, arr_d, balance, c_status, cancel, catering_only, checkin, checkin_pm… |
viewJqGrid_ho_rate | 36 | ALINCustomSelect_Plus30 | addrate, bar_code, child, crib, enddate, extra, holidaychild, holidaycrib… |
view_ho_guest_credit_card | 36 | ALINCustomSelect_Plus30 | acquirer, amount, authcode, bankname, batchno, cardnumber, cardtype, datetime… |
viewJqGrid_ho_gledger_coid_all | 35 | ALINCustomSelect_Plus30 | al, amount, ar, audit_status, bar, ca, cc, closedate… |
viewlistpms_ct_yygk_ct_lszd | 35 | ALINCustomSelect_Plus30 | DISPRE, JDH, JZRQ, JZSJ, KTRQ, KTSJ, MC, NO… |
viewGridPMS_ho_gledger_all | 35 | ALINCustomSelect_Plus30 | codate, coid, company, cotime, credit, date, dcode, debit… |
viewGridPMS_ho_arcust | 35 | ALINCustomSelect_Plus30 | allsearch, aname, ardate, ardate_c, balance_c, barcode, begindate, count_balance… |
viewJqGrid_ho_guest_listAll_andhis | 34 | ALINCustomSelect_Normal | adults, arr_d, arr_d_c, arr_t, exp_dep_d, exp_dep_d_c, exp_dep_t, fname… |
viewJqGrid_temp_BILLho_gledger_new | 34 | ALINCustomSelect_Plus30 | arbalance, as_remark, coid, couserid, cousername, credit, date, datetime_c… |
viewJqGrid_ho_gledger_coid | 34 | ALINCustomSelect_Plus30 | al, amount, ar, arr_d_c, audit_status, audit_status_c, audit_status_color, bar… |
viewGridPMS_ho_arledgerAll | 34 | ALINCustomSelect_Plus30 | codate_c, coid, credit, crtype, custom_keyword, date, date_c, dcode… |
ho_ratehd | 33 | ALINCustomSelect_Plus30 | OTA, OtaCancelRule, OtaCompanyRule, OtaCreateRule, OtaMarketRule, OtaRemark, addition, addition_tt… |
viewGridPMShotel_mainrpt_setup | 33 | ALINCustomSelect_Plus30 | bug1, bug10, bug11, bug12, bug2, bug3, bug4, bug5… |
viewGridPMS_ho_guest | 32 | ALINCustomSelect_Normal, ALINCustomSelect_Plus30, PMSReservationSearch | Status, adults, arr_d, child, comp_id, company, exp_dep_d, fname… |
viewGridPMS_ReservationOperator | 32 | ALINCustomSelect_Plus30 | DND, L, M, Status, T, agent, arr_d, balance… |
view_items_inventory_guest | 32 | ALINCustomSelect_Plus30 | Status, amount, arr_d, attribute, begindate, begindate_c, begintime, code… |
viewJqGrid_ho_AR_FR_AP_ledger_all | 32 | ALINCustomSelect_Plus30 | adjust, aname, co_text4, codate, coid, cotime, couserid, credit… |
ho_dcode | 32 | ALINCustomSelect_Plus30 | bmh, code, code_b, comm, crtype, d_grp, dc, dcode… |
viewGridPMS_message_list | 32 | ALINCustomSelect_Plus30 | arr_d, checkroomuser, chk_Cancel, chk_CheckOut, chk_DueIn, chk_DueOut, chk_InHouse, chk_NoShow… |
viewGridPMS_ho_guest_waitroom | 32 | ALINCustomSelect_Plus30 | adults, arr, arr_d, block, child, comp_features, crtime, cruserid… |
全部以 ALIN/Ho/List 前缀命名,按业务域划分:
通用 CRUD(框架级): ALINCustomSelect、ALINCustomSelect_Normal、ALINCustomSelect_Plus30、ALINCustomUpdate、ALINCustomDefault、ALINProcedure2Post
预订域: viewGridPMS_Reservation、BUSReservationSearchPlus、HoReservationRateDetail、ListRetreveGuestSelect
账务域: HoGledgerPackageList、HoARledgerSearch、HoBILLS_BillDefault3、HoMArledgerAndBootbymno
客户域: HoProfileFromIdentityCard、ListRetrieveHoProfilePersonAndBoot、ViewJqGrid_ho_marcust_arcust
审计/报表: AUDIT_120Search、HoGroupStatistics、ListRetrieveOutputStatistics
系统中所有查询条件都用 AES 加密传输,但密钥硬编码在客户端源码里:
// js/huasheng.js:127
var key = CryptoJs.enc.Utf8.parse("abcdefgabcdefg12");
var encrypted = CryptoJs.AES.encrypt(srcs, key,
{mode: CryptoJs.mode.ECB, padding: CryptoJs.pad.Pkcs7});
影响:密钥、模式(ECB)、填充全部已知且全客户端共用。任何人都能构造合法的加密请求体。
更严重:ECB 模式是确定性加密 —— 相同的明文产生相同的密文,攻击者无需解密即可识别重复模式。且密钥 abcdefgabcdefg12 是键盘顺序的弱密钥。
架构上 /qms/mobile/procedure2 是一个"传存储过程名 + 传条件"的通用执行器。前端 sqlwhere 参数在客户端拼装后加密发送,后端解密后直接使用。
代码中可见 292 处 SQL 条件字符串拼接(sqlwhere + ...)。加密只是"掩码"而非"防护" —— 因为密钥公开,攻击者可自行加密任意恶意条件。
风险:若后端未做参数化或存储过程内存在动态 SQL,将形成注入面。
/qms/mobile/procedure2withoutToken 出现 10 次 —— 从命名看是不校验 token 的接口变体。需确认后端是否真的只允许白名单操作,否则是未授权访问入口。
登录成功后,用户身份写入 localStorage(未加密):
localStorage.setItem("hotelId", hotelId);
localStorage.setItem("userid", data.userid);
localStorage.setItem("token", ...);
localStorage.setItem("kn", data.kn); // 内部标识
配合硬编码密钥,本机任意进程可读取并冒用会话。
本地 127.0.0.1:60050 服务暴露 6 类硬件能力,均无任何认证:
| 端点 | 功能 |
|---|---|
/readIdentityCard | 读取身份证 |
/readIdentityCardHttp | 读取身份证(HTTP) |
/FacePhoto | 人脸照片采集 |
/AvisionA6 | 证件扫描仪 |
/DoorCard | 门卡读写 |
/text2audio | 语音合成 |
本机任意程序均可调用,可直接读取身份证信息、写入门卡。
源码中残留开发环境地址:192.168.1.156:8080(模板默认),另有报表服务器 192.168.1.12:9090、192.168.33.1 等内网地址。
从 js/login/login.js 还原的完整登录链路:
① 用户输入 酒店编号 / 用户号 / 密码
↓
② POST {服务器}/huasheng/qms/mobile/getToken
→ 返回 token、userid、username、hotelname、kn
↓
③ 检查在线用户(onlineUser):若已登录 → 弹出占用提示,
可选择"强制登录"(login 操作)
↓
④ POST {服务器}/huasheng/qms/mobile/login_check
Headers: token, hotelId
→ RETURN=="1" 则登录成功
↓
⑤ 身份写入 localStorage:
hotelId / hotelname / loginTime / bmh / userid / kn / username
登录页支持"第三方登录"(/third/login-url),用于单点登录集成。
报表体系:自建 URL 协议做报表渲染,把 token/hotelId 拼在查询串里:
HSAPI_RPTURL = hsrpt(s)://{server}/huasheng/rpt?token=..&hotelId=..
HSAPI_RPTPBLURL / HSAPI_RPTEXCELURL / HSAPI_BILLURL(账单 PDF)
WebSocket:两个端点 /ws-sql-log/(SQL 日志)和 /ws-timer/(定时推送),后者用于房态实时刷新、预订数据同步。
支付集成:同时对接 /weChat/* 和 /al/*(支付宝)两套支付流程,各有 payment_temp(预授权)、payment_checkout(结账)、payment_refund(退款)、payment_credit(挂账)。
电子发票:/efd/bill、/efd/devices(Electronic Fiscal Device,税务开票设备对接)。
数据导出:/excel/convert、/excel/export64、/account/export、/hobill/bill-pdf。
更新机制:electron-updater 使用 generic provider,但 url 字段为空(未配置在线更新);残留 POS_setup_1.0.1.exe 的更新元数据(2020 年)。
① 判定应用类型 —— 先 file 看安装包格式:Nullsoft Installer → 用 7-Zip 解包。
② 解包安装器 —— 7z x installer.exe 得到 $PLUGINSDIR/app-64.7z(程序主体)。
③ 提取应用 —— 再解 app-64.7z。若见到 resources/app/ + *.dll + locales/,即为 Electron。
④ 源码定位 —— 有 resources/app.asar 就 asar extract;没有 asar 则源码直接裸露(本例)。
⑤ 找通信层 —— grep -rn "HSAPI_AJAXURL\s*=" 找基址,再 grep "HSAPI_[A-Z_]*\s*:" 拿端点常量表。
⑥ 挖数据模型 —— 找 *.db/*.json 配置,解析 tablename+fields+procedurename 字段,重建 schema。
⑦ 找密钥 —— grep -rnoE "Utf8\.parse\("[^"](8, 40)"\)" 一把捞出硬编码密钥。